Compare commits

...

9 commits

Author SHA1 Message Date
TrevKnows
84c2832807 Fully merged - no errors 2024-04-19 16:56:54 -04:00
TrevKnows
57b1eced76 Late Commit 2024-04-11 12:58:36 -04:00
TrevKnows
7e2ee8733b First Commit 2023-12-11 15:38:22 -05:00
TrevKnows
51e7ef0573 Testing 2023-08-08 06:55:08 -04:00
TrevKnows
2ecc9a2ec9 Added PTR to Bluetooth Module 2023-06-27 12:48:46 -04:00
TrevKnows
0646453175 Added PTR to Main and WiFi Modules 2023-06-27 12:33:40 -04:00
TrevKnows
a34e2bf6d3 Board ID Fix - Pass 1
BLE boards running CircuitPython 7+ are now compatible with PyLeap.

It was tested using Circuit Playground Bluefruit Express, CLUE nRF52840 Express, and Feather nRF52840.

I also added a placeholder for boards without specific images.
2023-05-24 00:59:59 -04:00
TrevKnows
e5bb6df771 First Commit 2023-05-17 12:08:13 -04:00
TrevKnows
976bf512fa Removed local reference from source file 2023-03-29 15:34:07 -04:00
106 changed files with 4536 additions and 1310 deletions

View file

@ -98,7 +98,7 @@ public class FileTransferClient {
// Set current peripheral
self.blePeripheral = blePeripheral
// Setup services
// Setup servic*es
let servicesGroup = DispatchGroup()
var error: Error? = nil

View file

@ -3,19 +3,19 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
D505B99C2755323C00386E9F /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D505B99B2755323C00386E9F /* NetworkMonitor.swift */; };
D505B99E2756894300386E9F /* ViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D505B99D2756894300386E9F /* ViewModifier.swift */; };
D50887632A2E35510002B798 /* Wifi_ifaddrs.m in Sources */ = {isa = PBXBuildFile; fileRef = D50887622A2E35510002B798 /* Wifi_ifaddrs.m */; };
D517F68126C5771D002996E8 /* FillerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D517F68026C5771D002996E8 /* FillerView.swift */; };
D5199A2F28DD16F100ACC34C /* BleContentTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5199A2E28DD16F100ACC34C /* BleContentTransfer.swift */; };
D51D1413293A53BD0028AEDD /* WifiCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */; };
D520D69029D4C9380022048D /* WifiServiceCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D520D68F29D4C9380022048D /* WifiServiceCellView.swift */; };
D520D69229D4C9900022048D /* WifiServiceCellSubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D520D69129D4C9900022048D /* WifiServiceCellSubView.swift */; };
D5267411292E902700D4C79E /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5267410292E902700D4C79E /* Networking.swift */; };
D5269C00291960A300C0CE4B /* WifiSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269BFF291960A300C0CE4B /* WifiSelection.swift */; };
D5269C02291997DE00C0CE4B /* WifiServiceCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */; };
D5269C042919985400C0CE4B /* WifiServiceCellSubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */; };
D5269C08291AB75800C0CE4B /* WifiPairingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C07291AB75800C0CE4B /* WifiPairingView.swift */; };
D52A926D29071DF400973B6B /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A926C29071DF400973B6B /* SelectionView.swift */; };
D52A926F29078E0A00973B6B /* WifiServiceSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */; };
@ -68,9 +68,32 @@
D58D887B26CC02B60085604A /* OnboardingViewPure.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D887A26CC02B60085604A /* OnboardingViewPure.swift */; };
D58E1C8828A2B10B00AB683E /* WifiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58E1C8728A2B10B00AB683E /* WifiView.swift */; };
D58E1C8A28A2B15E00AB683E /* WifiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58E1C8928A2B15E00AB683E /* WifiViewModel.swift */; };
D58E1C8D28A2B32C00AB683E /* Wifi_ifaddrs.m in Sources */ = {isa = PBXBuildFile; fileRef = D58E1C8C28A2B32C00AB683E /* Wifi_ifaddrs.m */; };
D58E1C8F28A30A5300AB683E /* WifiNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58E1C8E28A30A5300AB683E /* WifiNetworkService.swift */; };
D595FC2E2812C23D00569D8C /* Image Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D595FC2D2812C23D00569D8C /* Image Extension.swift */; };
D5974AF02BD2B04500498E0C /* KTScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AEF2BD2B04500498E0C /* KTScanner.swift */; };
D5974AF22BD2B07700498E0C /* KTFileTransferPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AF12BD2B07700498E0C /* KTFileTransferPeripheral.swift */; };
D5974AF42BD2B0A100498E0C /* KTDirectoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AF32BD2B0A100498E0C /* KTDirectoryEntry.swift */; };
D5974AF62BD2B0D500498E0C /* KTFileTransferClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AF52BD2B0D500498E0C /* KTFileTransferClient.swift */; };
D5974AF82BD2B0FB00498E0C /* KTBleFileTransferPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AF72BD2B0FB00498E0C /* KTBleFileTransferPeripheral.swift */; };
D5974AFA2BD2B13000498E0C /* KTDataProcessingQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AF92BD2B13000498E0C /* KTDataProcessingQueue.swift */; };
D5974AFC2BD2B15A00498E0C /* KTConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AFB2BD2B15A00498E0C /* KTConnectionManager.swift */; };
D5974AFF2BD2B48300498E0C /* KTBleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974AFE2BD2B48300498E0C /* KTBleManager.swift */; };
D5974B012BD2B4A200498E0C /* KTBleManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B002BD2B4A200498E0C /* KTBleManagerImpl.swift */; };
D5974B042BD2B4FA00498E0C /* KTBlePeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B032BD2B4FA00498E0C /* KTBlePeripheral.swift */; };
D5974B062BD2B52000498E0C /* KTBleAdvertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B052BD2B52000498E0C /* KTBleAdvertisement.swift */; };
D5974B082BD2B57300498E0C /* KTBleAdvertisement+ManufacturerAdafruit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B072BD2B57300498E0C /* KTBleAdvertisement+ManufacturerAdafruit.swift */; };
D5974B0A2BD2B59300498E0C /* KTSavedBondedBlePeripherals.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B092BD2B59300498E0C /* KTSavedBondedBlePeripherals.swift */; };
D5974B0D2BD2B5E200498E0C /* KTBlePeripheralScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B0C2BD2B5E200498E0C /* KTBlePeripheralScanner.swift */; };
D5974B0F2BD2B60C00498E0C /* KTBlePeripheralScannerFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B0E2BD2B60C00498E0C /* KTBlePeripheralScannerFake.swift */; };
D5974B112BD2B63200498E0C /* KTBlePeripheralScannerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B102BD2B63200498E0C /* KTBlePeripheralScannerImpl.swift */; };
D5974B132BD2B97300498E0C /* KTPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B122BD2B97300498E0C /* KTPeripheral.swift */; };
D5974B152BD2BE5300498E0C /* LogHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B142BD2BE5300498E0C /* LogHelper.swift */; };
D5974B182BD2D41700498E0C /* KTUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B172BD2D41700498E0C /* KTUtils.swift */; };
D5974B1A2BD2D43800498E0C /* KTResult+SimpleCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B192BD2D43800498E0C /* KTResult+SimpleCheck.swift */; };
D5974B1C2BD2D45600498E0C /* KTFileTransferPathUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B1B2BD2D45600498E0C /* KTFileTransferPathUtils.swift */; };
D5974B1E2BD2D47E00498E0C /* KTData+LittleEndianTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B1D2BD2D47E00498E0C /* KTData+LittleEndianTypes.swift */; };
D5974B202BD2D4D100498E0C /* KTCommandQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B1F2BD2D4D100498E0C /* KTCommandQueue.swift */; };
D5974B222BD2D4F000498E0C /* KTTypes+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5974B212BD2D4F000498E0C /* KTTypes+Data.swift */; };
D59DFD8F268A4A4D001737F6 /* BTConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59DFD8E268A4A4D001737F6 /* BTConnectionView.swift */; };
D59DFDB6268CD052001737F6 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59DFDB5268CD052001737F6 /* AppEnvironment.swift */; };
D59DFDBA268CDEEC001737F6 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59DFDB9268CDEEC001737F6 /* RootViewModel.swift */; };
@ -84,6 +107,8 @@
D5BA1F7F28B66F280012FC62 /* WifiServiceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA1F7E28B66F280012FC62 /* WifiServiceManager.swift */; };
D5BA1F8128B66F920012FC62 /* CircuitPythonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA1F8028B66F920012FC62 /* CircuitPythonService.swift */; };
D5BA1F8328B68ED40012FC62 /* NetworkPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BA1F8228B68ED40012FC62 /* NetworkPeripheral.swift */; };
D5BBD12C2A1538C100961B68 /* BoardDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BBD12B2A1538C100961B68 /* BoardDataProvider.swift */; };
D5BBD12E2A1C6AB300961B68 /* BleBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5BBD12D2A1C6AB300961B68 /* BleBannerView.swift */; };
D5C474AC27E174A5002DD160 /* WebView Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C474AB27E174A5002DD160 /* WebView Content.swift */; };
D5C474C827E39FD7002DD160 /* ReadexPro-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D5C474C627E39FC8002DD160 /* ReadexPro-Medium.ttf */; };
D5C474C927E39FDA002DD160 /* ReadexPro-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D5C474C727E39FC8002DD160 /* ReadexPro-Regular.ttf */; };
@ -116,13 +141,14 @@
D50237E3280994F900F1EE8A /* PyLeap.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = PyLeap.entitlements; path = "../../../../Desktop/Copy Of PyLeap Entitlement/PyLeap.entitlements"; sourceTree = "<group>"; };
D505B99B2755323C00386E9F /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
D505B99D2756894300386E9F /* ViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifier.swift; sourceTree = "<group>"; };
D50887612A2E35490002B798 /* PyLeap-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PyLeap-Bridging-Header.h"; sourceTree = "<group>"; };
D50887622A2E35510002B798 /* Wifi_ifaddrs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Wifi_ifaddrs.m; sourceTree = "<group>"; };
D517F68026C5771D002996E8 /* FillerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillerView.swift; sourceTree = "<group>"; };
D5199A2E28DD16F100ACC34C /* BleContentTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BleContentTransfer.swift; sourceTree = "<group>"; };
D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiCellViewModel.swift; sourceTree = "<group>"; };
D520D68F29D4C9380022048D /* WifiServiceCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiServiceCellView.swift; sourceTree = "<group>"; };
D520D69129D4C9900022048D /* WifiServiceCellSubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiServiceCellSubView.swift; sourceTree = "<group>"; };
D5267410292E902700D4C79E /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = "<group>"; };
D5269BFF291960A300C0CE4B /* WifiSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiSelection.swift; path = "PyLeap/Views/Unpaired View/WifiSelection.swift"; sourceTree = SOURCE_ROOT; };
D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiServiceCellView.swift; path = ../../../../../../Desktop/WifiServiceCellView.swift; sourceTree = "<group>"; };
D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiServiceCellSubView.swift; path = ../../../../../../Desktop/WifiServiceCellSubView.swift; sourceTree = "<group>"; };
D5269C07291AB75800C0CE4B /* WifiPairingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiPairingView.swift; sourceTree = "<group>"; };
D52A926C29071DF400973B6B /* SelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = "<group>"; };
D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiServiceSelectionView.swift; sourceTree = "<group>"; };
@ -177,9 +203,33 @@
D58D887A26CC02B60085604A /* OnboardingViewPure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewPure.swift; sourceTree = "<group>"; };
D58E1C8728A2B10B00AB683E /* WifiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiView.swift; sourceTree = "<group>"; };
D58E1C8928A2B15E00AB683E /* WifiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiViewModel.swift; sourceTree = "<group>"; };
D58E1C8C28A2B32C00AB683E /* Wifi_ifaddrs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Wifi_ifaddrs.m; sourceTree = "<group>"; };
D58E1C8E28A30A5300AB683E /* WifiNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiNetworkService.swift; sourceTree = "<group>"; };
D595FC2D2812C23D00569D8C /* Image Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image Extension.swift"; sourceTree = "<group>"; };
D5974AED2BD2AF4F00498E0C /* Note.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Note.md; sourceTree = "<group>"; };
D5974AEF2BD2B04500498E0C /* KTScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTScanner.swift; sourceTree = "<group>"; };
D5974AF12BD2B07700498E0C /* KTFileTransferPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTFileTransferPeripheral.swift; sourceTree = "<group>"; };
D5974AF32BD2B0A100498E0C /* KTDirectoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTDirectoryEntry.swift; sourceTree = "<group>"; };
D5974AF52BD2B0D500498E0C /* KTFileTransferClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTFileTransferClient.swift; sourceTree = "<group>"; };
D5974AF72BD2B0FB00498E0C /* KTBleFileTransferPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBleFileTransferPeripheral.swift; sourceTree = "<group>"; };
D5974AF92BD2B13000498E0C /* KTDataProcessingQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTDataProcessingQueue.swift; sourceTree = "<group>"; };
D5974AFB2BD2B15A00498E0C /* KTConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTConnectionManager.swift; sourceTree = "<group>"; };
D5974AFE2BD2B48300498E0C /* KTBleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBleManager.swift; sourceTree = "<group>"; };
D5974B002BD2B4A200498E0C /* KTBleManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBleManagerImpl.swift; sourceTree = "<group>"; };
D5974B032BD2B4FA00498E0C /* KTBlePeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBlePeripheral.swift; sourceTree = "<group>"; };
D5974B052BD2B52000498E0C /* KTBleAdvertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBleAdvertisement.swift; sourceTree = "<group>"; };
D5974B072BD2B57300498E0C /* KTBleAdvertisement+ManufacturerAdafruit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KTBleAdvertisement+ManufacturerAdafruit.swift"; sourceTree = "<group>"; };
D5974B092BD2B59300498E0C /* KTSavedBondedBlePeripherals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTSavedBondedBlePeripherals.swift; sourceTree = "<group>"; };
D5974B0C2BD2B5E200498E0C /* KTBlePeripheralScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBlePeripheralScanner.swift; sourceTree = "<group>"; };
D5974B0E2BD2B60C00498E0C /* KTBlePeripheralScannerFake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBlePeripheralScannerFake.swift; sourceTree = "<group>"; };
D5974B102BD2B63200498E0C /* KTBlePeripheralScannerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTBlePeripheralScannerImpl.swift; sourceTree = "<group>"; };
D5974B122BD2B97300498E0C /* KTPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTPeripheral.swift; sourceTree = "<group>"; };
D5974B142BD2BE5300498E0C /* LogHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogHelper.swift; sourceTree = "<group>"; };
D5974B172BD2D41700498E0C /* KTUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTUtils.swift; sourceTree = "<group>"; };
D5974B192BD2D43800498E0C /* KTResult+SimpleCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KTResult+SimpleCheck.swift"; sourceTree = "<group>"; };
D5974B1B2BD2D45600498E0C /* KTFileTransferPathUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTFileTransferPathUtils.swift; sourceTree = "<group>"; };
D5974B1D2BD2D47E00498E0C /* KTData+LittleEndianTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KTData+LittleEndianTypes.swift"; sourceTree = "<group>"; };
D5974B1F2BD2D4D100498E0C /* KTCommandQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KTCommandQueue.swift; sourceTree = "<group>"; };
D5974B212BD2D4F000498E0C /* KTTypes+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KTTypes+Data.swift"; sourceTree = "<group>"; };
D59DFD8E268A4A4D001737F6 /* BTConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTConnectionView.swift; sourceTree = "<group>"; };
D59DFDB5268CD052001737F6 /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
D59DFDB9268CDEEC001737F6 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
@ -194,6 +244,8 @@
D5BA1F7E28B66F280012FC62 /* WifiServiceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiServiceManager.swift; sourceTree = "<group>"; };
D5BA1F8028B66F920012FC62 /* CircuitPythonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircuitPythonService.swift; sourceTree = "<group>"; };
D5BA1F8228B68ED40012FC62 /* NetworkPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkPeripheral.swift; sourceTree = "<group>"; };
D5BBD12B2A1538C100961B68 /* BoardDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardDataProvider.swift; sourceTree = "<group>"; };
D5BBD12D2A1C6AB300961B68 /* BleBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BleBannerView.swift; sourceTree = "<group>"; };
D5C474AB27E174A5002DD160 /* WebView Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebView Content.swift"; sourceTree = "<group>"; };
D5C474C227E39FAD002DD160 /* ReadexPro-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ReadexPro-Bold.ttf"; sourceTree = "<group>"; };
D5C474C527E39FC8002DD160 /* ReadexPro-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ReadexPro-Light.ttf"; sourceTree = "<group>"; };
@ -218,7 +270,6 @@
D5DD39A628D11817000FAEB8 /* WifiFileTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiFileTransfer.swift; sourceTree = "<group>"; };
D5DD39A828D11962000FAEB8 /* WifiTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiTransferService.swift; sourceTree = "<group>"; };
D5DD39AA28D234C3000FAEB8 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
D5ECF4B828C8E3C600FBF93D /* PyLeap-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PyLeap-Bridging-Header.h"; sourceTree = "<group>"; };
D5F53CEA2694B524007634C2 /* Blinka Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blinka Animation.swift"; sourceTree = "<group>"; };
D5F53CEC2694B7A9007634C2 /* OnboardingBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -278,9 +329,12 @@
D59DFDB5268CD052001737F6 /* AppEnvironment.swift */,
D534F3FB280B59090053699C /* ExampleView.swift */,
D544A2502822D4730038D483 /* Spotlight Extension.swift */,
D5974AEC2BD2AEE200498E0C /* Updated AdafruitKit */,
D5BBD12A2A15389400961B68 /* Helpers */,
D567E2B428B80DC10009F768 /* Outline.md */,
D567E2DD28C8D3E20009F768 /* SettingsView */,
D5C74DF027EB92E900730505 /* Model */,
D59DFDB2268CCEAC001737F6 /* Views */,
D59DFDB2268CCEAC001737F6 /* Features */,
D5C74DEE27EB91D600730505 /* Networking */,
D5C74DED27EB919200730505 /* Resource */,
D5C41D6726C5F509004C38E3 /* Download View */,
@ -376,23 +430,29 @@
path = BLESetttings;
sourceTree = "<group>";
};
D58E1C8628A2B0DE00AB683E /* Wifi View */ = {
D56FFE1C2A2A4ED500EF1E3B /* Views */ = {
isa = PBXGroup;
children = (
D567E2DD28C8D3E20009F768 /* SettingsView */,
D58E1C8728A2B10B00AB683E /* WifiView.swift */,
D567E2B728C137880009F768 /* WifiCell.swift */,
D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */,
D567E2B928C1382E0009F768 /* WifiSubViewCell.swift */,
);
path = Views;
sourceTree = "<group>";
};
D58E1C8628A2B0DE00AB683E /* Wifi Module */ = {
isa = PBXGroup;
children = (
D56FFE1C2A2A4ED500EF1E3B /* Views */,
D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */,
D58E1C8928A2B15E00AB683E /* WifiViewModel.swift */,
D5269C07291AB75800C0CE4B /* WifiPairingView.swift */,
D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */,
D5269BFF291960A300C0CE4B /* WifiSelection.swift */,
D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */,
D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */,
D520D68F29D4C9380022048D /* WifiServiceCellView.swift */,
D5BA1F7E28B66F280012FC62 /* WifiServiceManager.swift */,
D5DD39A628D11817000FAEB8 /* WifiFileTransfer.swift */,
D5DD39A828D11962000FAEB8 /* WifiTransferService.swift */,
D520D69129D4C9900022048D /* WifiServiceCellSubView.swift */,
D5AA27F728CA785B001CCE25 /* CircuitPythonType.swift */,
D5482F4A28E75053000B0C8E /* LocalNetworkAuth.swift */,
D5AA27F928CA8D46001CCE25 /* WifiStatusHeaderBarView.swift */,
@ -402,17 +462,89 @@
D5BA1F8028B66F920012FC62 /* CircuitPythonService.swift */,
D58E1C8E28A30A5300AB683E /* WifiNetworkService.swift */,
D5BA1F8228B68ED40012FC62 /* NetworkPeripheral.swift */,
D50887622A2E35510002B798 /* Wifi_ifaddrs.m */,
D50887612A2E35490002B798 /* PyLeap-Bridging-Header.h */,
D5D7DF2A28B3E321008552D1 /* BasicCredentials.swift */,
D58E1C8C28A2B32C00AB683E /* Wifi_ifaddrs.m */,
D5ECF4B828C8E3C600FBF93D /* PyLeap-Bridging-Header.h */,
);
path = "Wifi View";
path = "Wifi Module";
sourceTree = "<group>";
};
D59DFDB2268CCEAC001737F6 /* Views */ = {
D5974AEC2BD2AEE200498E0C /* Updated AdafruitKit */ = {
isa = PBXGroup;
children = (
D58E1C8628A2B0DE00AB683E /* Wifi View */,
D5974B162BD2D3E800498E0C /* Utils */,
D5974AEE2BD2B02000498E0C /* KTFileTransfer */,
D5974AED2BD2AF4F00498E0C /* Note.md */,
D5974AFB2BD2B15A00498E0C /* KTConnectionManager.swift */,
D5974AFD2BD2B44600498E0C /* KTBle */,
D5974B142BD2BE5300498E0C /* LogHelper.swift */,
);
path = "Updated AdafruitKit";
sourceTree = "<group>";
};
D5974AEE2BD2B02000498E0C /* KTFileTransfer */ = {
isa = PBXGroup;
children = (
D5974AEF2BD2B04500498E0C /* KTScanner.swift */,
D5974AF12BD2B07700498E0C /* KTFileTransferPeripheral.swift */,
D5974AF32BD2B0A100498E0C /* KTDirectoryEntry.swift */,
D5974AF52BD2B0D500498E0C /* KTFileTransferClient.swift */,
D5974AF72BD2B0FB00498E0C /* KTBleFileTransferPeripheral.swift */,
D5974AF92BD2B13000498E0C /* KTDataProcessingQueue.swift */,
);
path = KTFileTransfer;
sourceTree = "<group>";
};
D5974AFD2BD2B44600498E0C /* KTBle */ = {
isa = PBXGroup;
children = (
D5974AFE2BD2B48300498E0C /* KTBleManager.swift */,
D5974B122BD2B97300498E0C /* KTPeripheral.swift */,
D5974B0B2BD2B5C300498E0C /* Scanner */,
D5974B002BD2B4A200498E0C /* KTBleManagerImpl.swift */,
D5974B022BD2B4C300498E0C /* KTPeripheral */,
);
path = KTBle;
sourceTree = "<group>";
};
D5974B022BD2B4C300498E0C /* KTPeripheral */ = {
isa = PBXGroup;
children = (
D5974B032BD2B4FA00498E0C /* KTBlePeripheral.swift */,
D5974B052BD2B52000498E0C /* KTBleAdvertisement.swift */,
D5974B072BD2B57300498E0C /* KTBleAdvertisement+ManufacturerAdafruit.swift */,
D5974B092BD2B59300498E0C /* KTSavedBondedBlePeripherals.swift */,
);
path = KTPeripheral;
sourceTree = "<group>";
};
D5974B0B2BD2B5C300498E0C /* Scanner */ = {
isa = PBXGroup;
children = (
D5974B0C2BD2B5E200498E0C /* KTBlePeripheralScanner.swift */,
D5974B102BD2B63200498E0C /* KTBlePeripheralScannerImpl.swift */,
D5974B0E2BD2B60C00498E0C /* KTBlePeripheralScannerFake.swift */,
);
path = Scanner;
sourceTree = "<group>";
};
D5974B162BD2D3E800498E0C /* Utils */ = {
isa = PBXGroup;
children = (
D5974B172BD2D41700498E0C /* KTUtils.swift */,
D5974B192BD2D43800498E0C /* KTResult+SimpleCheck.swift */,
D5974B1B2BD2D45600498E0C /* KTFileTransferPathUtils.swift */,
D5974B1D2BD2D47E00498E0C /* KTData+LittleEndianTypes.swift */,
D5974B1F2BD2D4D100498E0C /* KTCommandQueue.swift */,
D5974B212BD2D4F000498E0C /* KTTypes+Data.swift */,
);
path = Utils;
sourceTree = "<group>";
};
D59DFDB2268CCEAC001737F6 /* Features */ = {
isa = PBXGroup;
children = (
D58E1C8628A2B0DE00AB683E /* Wifi Module */,
D5507ACE26C668BC00512BAA /* UI Components */,
D59DFDB3268CCEB9001737F6 /* Onboarding Views */,
D5C474AA27E1746F002DD160 /* WebView */,
@ -424,7 +556,7 @@
D517F68026C5771D002996E8 /* FillerView.swift */,
D5D1F4AC27ECFD200040E2BF /* Startup View */,
);
path = Views;
path = Features;
sourceTree = "<group>";
};
D59DFDB3268CCEB9001737F6 /* Onboarding Views */ = {
@ -438,6 +570,14 @@
path = "Onboarding Views";
sourceTree = "<group>";
};
D5BBD12A2A15389400961B68 /* Helpers */ = {
isa = PBXGroup;
children = (
D5BBD12B2A1538C100961B68 /* BoardDataProvider.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
D5C41D6726C5F509004C38E3 /* Download View */ = {
isa = PBXGroup;
children = (
@ -535,6 +675,7 @@
children = (
D59E31A9281B8DD300D24211 /* DownloadState.swift */,
D5507ACB26C668BC00512BAA /* BleModuleView.swift */,
D5BBD12D2A1C6AB300961B68 /* BleBannerView.swift */,
D5507ACC26C668BC00512BAA /* BleModuleViewModel.swift */,
D5199A2E28DD16F100ACC34C /* BleContentTransfer.swift */,
D5D5BB3828DD19F000E5D93F /* BleContentCommands.swift */,
@ -605,8 +746,9 @@
D52F7E682672F4C400911D43 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1250;
LastUpgradeCheck = 1250;
LastUpgradeCheck = 1500;
TargetAttributes = {
D52F7E6F2672F4C400911D43 = {
CreatedOnToolsVersion = 12.5;
@ -659,14 +801,21 @@
D5D1F4B827ED2F4F0040E2BF /* FileManagerCheck.swift in Sources */,
D52BE82A26A0660200630900 /* KeyboardUtils.swift in Sources */,
D58182FB27F732E40091C43B /* SubCellViewModel.swift in Sources */,
D520D69229D4C9900022048D /* WifiServiceCellSubView.swift in Sources */,
D5D5BB3928DD19F000E5D93F /* BleContentCommands.swift in Sources */,
D544A1D2281B9BB70038D483 /* Buttons.swift in Sources */,
D5974B1A2BD2D43800498E0C /* KTResult+SimpleCheck.swift in Sources */,
D5D1F4AE27ECFDA10040E2BF /* GifImage.swift in Sources */,
D5974AFA2BD2B13000498E0C /* KTDataProcessingQueue.swift in Sources */,
D5974B0A2BD2B59300498E0C /* KTSavedBondedBlePeripherals.swift in Sources */,
D58E1C8A28A2B15E00AB683E /* WifiViewModel.swift in Sources */,
D5974B062BD2B52000498E0C /* KTBleAdvertisement.swift in Sources */,
D5267411292E902700D4C79E /* Networking.swift in Sources */,
D5974B1C2BD2D45600498E0C /* KTFileTransferPathUtils.swift in Sources */,
D5C474AC27E174A5002DD160 /* WebView Content.swift in Sources */,
D544A2512822D4730038D483 /* Spotlight Extension.swift in Sources */,
D56F640D270242CA000E5975 /* FileTransferPathUtils.swift in Sources */,
D5974AF82BD2B0FB00498E0C /* KTBleFileTransferPeripheral.swift in Sources */,
D5507AD326C668BC00512BAA /* BleModuleView.swift in Sources */,
D58182FD27FBF09F0091C43B /* BLEPairingView.swift in Sources */,
D5BA1F8128B66F920012FC62 /* CircuitPythonService.swift in Sources */,
@ -678,10 +827,12 @@
D56F640C270242CA000E5975 /* UIColor+LightAndDark.swift in Sources */,
D5F53CED2694B7A9007634C2 /* OnboardingBackgroundView.swift in Sources */,
D5D1F4B227ECFF760040E2BF /* ProjectsModel.swift in Sources */,
D5269C02291997DE00C0CE4B /* WifiServiceCellView.swift in Sources */,
D5BA1F7A28B52A490012FC62 /* WifiListDetailView.swift in Sources */,
D5974AFF2BD2B48300498E0C /* KTBleManager.swift in Sources */,
D52F7E742672F4C400911D43 /* PyLeapApp.swift in Sources */,
D5BBD12E2A1C6AB300961B68 /* BleBannerView.swift in Sources */,
D567E2B628B81B730009F768 /* Queue.swift in Sources */,
D5974B132BD2B97300498E0C /* KTPeripheral.swift in Sources */,
D534F3FC280B59090053699C /* ExampleView.swift in Sources */,
D57858ED28327E18008E8BE4 /* PairingTutorialView.swift in Sources */,
D58358EE27DA5C0F0069F7F5 /* NetworkError.swift in Sources */,
@ -689,6 +840,7 @@
D58E1C8828A2B10B00AB683E /* WifiView.swift in Sources */,
D5D1F4A627EBA9C80040E2BF /* DemoSubCellView.swift in Sources */,
D57858F12832D9C8008E8BE4 /* CreditView.swift in Sources */,
D5974B012BD2B4A200498E0C /* KTBleManagerImpl.swift in Sources */,
D59DFDB6268CD052001737F6 /* AppEnvironment.swift in Sources */,
D5CC6BB428173AE0008629FB /* HeaderView.swift in Sources */,
D52BE85626A0E5A700630900 /* PeripheralAutoConnect.swift in Sources */,
@ -702,34 +854,44 @@
D5DD39A728D11817000FAEB8 /* WifiFileTransfer.swift in Sources */,
D5AA27FA28CA8D46001CCE25 /* WifiStatusHeaderBarView.swift in Sources */,
D5F53CEB2694B524007634C2 /* Blinka Animation.swift in Sources */,
D5269C00291960A300C0CE4B /* WifiSelection.swift in Sources */,
D5597BF826A9E14B00DF17C0 /* AppDelegate.swift in Sources */,
D56F640E270242CA000E5975 /* String+DeletingPrefix.swift in Sources */,
D57858F328333CBC008E8BE4 /* TroubleshootView.swift in Sources */,
D5C6D42128C8EA0700F5C7C9 /* WifiHeaderView.swift in Sources */,
D5507AD026C668BC00512BAA /* BleModuleViewModel.swift in Sources */,
D5974B112BD2B63200498E0C /* KTBlePeripheralScannerImpl.swift in Sources */,
D5482F4928E63DB7000B0C8E /* MainSelectionViewModel.swift in Sources */,
D5974B0D2BD2B5E200498E0C /* KTBlePeripheralScanner.swift in Sources */,
D5597C3B26B98E1E00DF17C0 /* NumbersOnly.swift in Sources */,
D5974AFC2BD2B15A00498E0C /* KTConnectionManager.swift in Sources */,
D5974B182BD2D41700498E0C /* KTUtils.swift in Sources */,
D5D1F4A427EBA7E30040E2BF /* NetworkManager.swift in Sources */,
D5974B082BD2B57300498E0C /* KTBleAdvertisement+ManufacturerAdafruit.swift in Sources */,
D5974B1E2BD2D47E00498E0C /* KTData+LittleEndianTypes.swift in Sources */,
D56B75D6294BAACE00D008E7 /* BLESettingsViewModel.swift in Sources */,
D5DD39AB28D234C3000FAEB8 /* SettingsViewModel.swift in Sources */,
D52A926F29078E0A00973B6B /* WifiServiceSelectionView.swift in Sources */,
D5BA1F8328B68ED40012FC62 /* NetworkPeripheral.swift in Sources */,
D59DFDBC268CE0EB001737F6 /* RootView.swift in Sources */,
D5974B0F2BD2B60C00498E0C /* KTBlePeripheralScannerFake.swift in Sources */,
D595FC2E2812C23D00569D8C /* Image Extension.swift in Sources */,
D567E2BA28C1382E0009F768 /* WifiSubViewCell.swift in Sources */,
D59DFD8F268A4A4D001737F6 /* BTConnectionView.swift in Sources */,
D58D887B26CC02B60085604A /* OnboardingViewPure.swift in Sources */,
D5361098296F5E5400228E15 /* JSONDecoderHelper.swift in Sources */,
D52BE85426A0E39100630900 /* BTConnectionViewModel.swift in Sources */,
D5974AF42BD2B0A100498E0C /* KTDirectoryEntry.swift in Sources */,
D5DD39A928D11962000FAEB8 /* WifiTransferService.swift in Sources */,
D5974AF62BD2B0D500498E0C /* KTFileTransferClient.swift in Sources */,
D59DFDC2268CFA36001737F6 /* OnboardingStepView.swift in Sources */,
D5CC6BAE2816FD27008629FB /* Config.swift in Sources */,
D544A24F282046840038D483 /* OnAnimationComplete.swift in Sources */,
D5507AD126C668BC00512BAA /* SearchBarView.swift in Sources */,
D59DFDBA268CDEEC001737F6 /* RootViewModel.swift in Sources */,
D536109A296FB2BB00228E15 /* DataStore.swift in Sources */,
D5974B202BD2D4D100498E0C /* KTCommandQueue.swift in Sources */,
D5C74DF527EB93E300730505 /* DemoViewCell.swift in Sources */,
D5974B152BD2BE5300498E0C /* LogHelper.swift in Sources */,
D567E2DF28C8D40C0009F768 /* SettingsView.swift in Sources */,
D5482F4B28E75053000B0C8E /* LocalNetworkAuth.swift in Sources */,
D567E2BC28C1527F0009F768 /* WifiSubViewCellModel.swift in Sources */,
@ -738,19 +900,24 @@
D5597C0226ADDCE300DF17C0 /* StartupView.swift in Sources */,
D567E2B828C137880009F768 /* WifiCell.swift in Sources */,
D5D1F4B027ECFDE00040E2BF /* NavBarModifier.swift in Sources */,
D5974B222BD2D4F000498E0C /* KTTypes+Data.swift in Sources */,
D5974AF02BD2B04500498E0C /* KTScanner.swift in Sources */,
D5597C0C26AF018800DF17C0 /* View+If.swift in Sources */,
D58E1C8D28A2B32C00AB683E /* Wifi_ifaddrs.m in Sources */,
D56B75D4294BAAB400D008E7 /* BLESettingsView.swift in Sources */,
D50887632A2E35510002B798 /* Wifi_ifaddrs.m in Sources */,
D52BE7EE269DF36E00630900 /* DownloadViewModel.swift in Sources */,
D59E31AA281B8DD300D24211 /* DownloadState.swift in Sources */,
D520D69029D4C9380022048D /* WifiServiceCellView.swift in Sources */,
D5974AF22BD2B07700498E0C /* KTFileTransferPeripheral.swift in Sources */,
D5BA1F7F28B66F280012FC62 /* WifiServiceManager.swift in Sources */,
D5C74DF327EB92FA00730505 /* View+Extensions.swift in Sources */,
D5AA27F828CA785B001CCE25 /* CircuitPythonType.swift in Sources */,
D5974B042BD2B4FA00498E0C /* KTBlePeripheral.swift in Sources */,
D5199A2F28DD16F100ACC34C /* BleContentTransfer.swift in Sources */,
D535E21628E1FA910096E548 /* ScrollRefreshableView.swift in Sources */,
D5640216271B54BF00AE1519 /* MainSelectionView.swift in Sources */,
D5269C042919985400C0CE4B /* WifiServiceCellSubView.swift in Sources */,
D505B99C2755323C00386E9F /* NetworkMonitor.swift in Sources */,
D5BBD12C2A1538C100961B68 /* BoardDataProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -794,6 +961,7 @@
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@ -855,6 +1023,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -886,17 +1055,17 @@
DEVELOPMENT_TEAM = 2X94RM7457;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PyLeap/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.adafruit.PyLeap;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PyLeap/Views/Wifi View/PyLeap-Bridging-Header.h";
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -916,17 +1085,17 @@
DEVELOPMENT_TEAM = 2X94RM7457;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PyLeap/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = com.adafruit.PyLeap;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "PyLeap/Views/Wifi View/PyLeap-Bridging-Header.h";
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "Default device image.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -204,7 +204,7 @@ class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate
guard let httpResponse = downloadTask.response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print ("server error")
print ("server error: \(String(describing: downloadTask.response))")
NotificationCenter.default.post(name: .downloadErrorDidOccur, object: nil, userInfo: nil)
return
}

View file

@ -64,20 +64,20 @@ class BTConnectionViewModel: ObservableObject {
// MARK: - Scanning Actions
private func startScanning() {
updateScannedPeripherals()
// Start scannning
BlePeripheral.rssiRunningAverageFactor = Self.kRssiRunningAverageFactor // Use running average for rssi
if !bleManager.isScanning {
bleManager.startScan()
connectionStatus = .scanning
}
// Start autoreconnect timer
autoreconnectTimer = Timer.scheduledTimer(withTimeInterval: Self.kRepeatTimeForForcedAutoreconnect, repeats: true, block: { timer in
DLog("Scan periodic autoreconnect check...")
FileTransferConnectionManager.shared.reconnect()
})
// updateScannedPeripherals()
//
// // Start scannning
// KTBlePeripheral.rssiRunningAverageFactor = Self.kRssiRunningAverageFactor // Use running average for rssi
// if !bleManager.isScanning {
// bleManager.startScan()
// connectionStatus = .scanning
// }
//
// // Start autoreconnect timer
// autoreconnectTimer = Timer.scheduledTimer(withTimeInterval: Self.kRepeatTimeForForcedAutoreconnect, repeats: true, block: { timer in
// DLog("Scan periodic autoreconnect check...")
// FileTransferConnectionManager.shared.reconnect()
// })
}
private func stopScanning() {
@ -120,9 +120,9 @@ class BTConnectionViewModel: ObservableObject {
// MARK: - Connections
private func connect(peripheral: BlePeripheral) {
// Connect to selected peripheral
selectedPeripheral = peripheral
bleManager.connect(to: peripheral)
// // Connect to selected peripheral
// selectedPeripheral = peripheral
// bleManager.connect(to: peripheral)
}
private func disconnect(peripheral: BlePeripheral) {
@ -241,7 +241,7 @@ class BTConnectionViewModel: ObservableObject {
}
private func peripheralDidUpdateName(notification: Notification) {
let name = notification.userInfo?[BlePeripheral.NotificationUserInfoKey.name.rawValue] as? String
let name = notification.userInfo?[KTBlePeripheral.NotificationUserInfoKey.name.rawValue] as? String
DLog("centralManager peripheralDidUpdateName: \(name ?? "<unknown>")")
}
}

View file

@ -0,0 +1,47 @@
//
// BleBannerView.swift
// PyLeap
//
// Created by Trevor Beaton on 5/22/23.
//
import SwiftUI
struct BleBannerView: View {
var deviceName: String
var disconnectAction: () -> Void
var body: some View {
VStack {
HStack {
Image("bluetoothLogo")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
Text(deviceName)
.font(Font.custom("ReadexPro-Regular", size: 14))
Button(action: disconnectAction) {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
}
}
// .background(GeometryReader {
// Color.clear.preference(key: ViewHeightKey.self,
// value: $0.frame(in: .local).size.height)
// })
}
}
}
struct BleBannerView_Previews: PreviewProvider {
static var previews: some View {
BleBannerView(deviceName: "Test", disconnectAction: {
print("Dismiss Action")
})
}
}

View file

@ -13,8 +13,8 @@ class BleContentCommands: ObservableObject {
private weak var fileTransferClient: FileTransferClient?
@Published var transmissionProgress: TransmissionProgress?
@Published var isTransmiting = false
@Published var bootUpInfo = String()
@Published var counter = 0
enum ProjectViewError: LocalizedError {
@ -63,7 +63,6 @@ class BleContentCommands: ObservableObject {
@Published var lastTransmit: TransmissionLog? = TransmissionLog(type: .write(size: 334))
@Published var activeAlert: ActiveAlert?
// Data
private let bleManager = BleManager.shared
@ -102,10 +101,7 @@ class BleContentCommands: ObservableObject {
case .success(let data):
self.lastTransmit = TransmissionLog(type: .read(data: data))
let str = String(decoding: data, as: UTF8.self)
print("Read: \(str)")
self.bootUpInfo = str
sharedBootinfo = str
print("Read: \(str)")
case .failure(let error):
self.lastTransmit = TransmissionLog(type: .error(message: error.localizedDescription))

View file

@ -8,7 +8,24 @@
import SwiftUI
import FileTransferClient
class BleContentTransfer: ObservableObject {
protocol BoardInfoDelegate: AnyObject {
func boardInfoDidUpdate(to newBoard: Board?)
}
enum ListCommandError: Error {
case belowMinimum
case isPrime
}
class BleContentTransfer: ObservableObject, BoardInfoDelegate {
@Published var currentBoard: Board? {
didSet {
}
}
static let shared = BleContentTransfer()
private weak var fileTransferClient: FileTransferClient?
@ -22,7 +39,6 @@ class BleContentTransfer: ObservableObject {
@Published var downloadState: DownloadState = .idle
@State var circuitPythonVersion = String()
@Published var isTransmiting = false
@ -31,19 +47,14 @@ class BleContentTransfer: ObservableObject {
@Published var transferError = false
@Published var downloaderror = false
@Published var bootUpInfo = ""
@Published var contentCommands = BleContentCommands()
// CLEAN UP
var projectDirectories: [URL] = []
var projectFiles: [URL] = []
var returnedArray = [[String]]()
var fileTransferArray : [URL] = []
var filesReadyForTransfer : [URL] = []
@Published var sendingBundle = false
@Published var didCompleteTranfer = false
@ -57,11 +68,13 @@ class BleContentTransfer: ObservableObject {
@Published var isConnectedToInternet = false
@Published var showAlert = false
var downloadPhases: String = ""
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
func boardInfoDidUpdate(to newBoard: Board?) {
self.currentBoard = newBoard
// Add any other logic you need to handle the new board here.
}
enum ProjectViewError: LocalizedError {
case fileTransferUndefined
@ -77,25 +90,9 @@ class BleContentTransfer: ObservableObject {
}
init() {
getCPVersion()
registerNotification(enabled: true)
}
func getCPVersion() {
if sharedBootinfo.contains("CircuitPython 7") {
print("circuitPythonVersion = 7")
circuitPythonVersion = "7"
print(circuitPythonVersion)
}
if sharedBootinfo.contains("CircuitPython 8") {
print("circuitPythonVersion = 8")
circuitPythonVersion = "8"
print(circuitPythonVersion)
}
}
private weak var didCompleteZip: NSObjectProtocol?
private weak var didEncounterTransferError: NSObjectProtocol?
private weak var downloadErrorDidOccur: NSObjectProtocol?
@ -310,14 +307,7 @@ class BleContentTransfer: ObservableObject {
}
}
}
// filterCPVersion(incomingArray: regularFileUrls)
// filterCPFiles(filesArray: regularFileUrls)
// pathManipulation(arrayOfAny: filterCPVersion(incomingArray: projectDirectories), regularArray: filterCPVersion(incomingArray: projectFiles))
projectDirectories.removeFirst()
DispatchQueue.main.async {
@ -336,70 +326,28 @@ class BleContentTransfer: ObservableObject {
func filterCPVersion(incomingArray: [URL]) -> [URL] {
print("project name \(projectName)")
for i in incomingArray {
print("incoming Array \(i.absoluteString)")
let filteredList = incomingArray.filter {
let lastPathComponent = $0.lastPathComponent
return lastPathComponent != "CircuitPython 8.x"
&& lastPathComponent != "CircuitPython 7.x"
&& lastPathComponent != "CircuitPython_Templates"
}
let listWithoutCP8Folder = incomingArray.filter {
$0.lastPathComponent != ("CircuitPython 8.x")
}
let listWithoutCP7Folder = listWithoutCP8Folder.filter {
$0.lastPathComponent != ("CircuitPython 7.x")
}
//CircuitPython_Templates
let removedCPTemplates = listWithoutCP7Folder.filter {
$0.lastPathComponent != ("CircuitPython_Templates")
}
if sharedBootinfo.contains("CircuitPython 8") {
let listForCurrentCPVersion = removedCPTemplates.filter {
!$0.absoluteString.contains("CircuitPython%207.x")
}
for i in listForCurrentCPVersion {
print("listForCurrentCPVersion :- \(i.absoluteString)")
}
return listForCurrentCPVersion
}
if sharedBootinfo.contains("CircuitPython 7") {
let listForCurrentCPVersion = listWithoutCP7Folder.filter {
!$0.absoluteString.contains("CircuitPython%208.x")
}
for i in listForCurrentCPVersion {
print("listForCurrentCPVersion :- \(i.absoluteString)")
}
let listForCurrentCPVersion = filteredList.filter {
$0.absoluteString.contains("CircuitPython%20\(Board.shared.versionNumber).x")
}
return listForCurrentCPVersion
}
return listWithoutCP7Folder
}
func makeDirectory(directoryArray: [URL], regularFilesArray: [URL]) {
print("\(#function) @Line: \(#line)")
var recursiveArray = directoryArray
for i in recursiveArray {
print("recursiveArray \(i.absoluteString)")
}
if directoryArray.isEmpty {
print("Array is empty. makeDirectory is done - Ready for file transfer!")
newTransfer(listOf: regularFilesArray)
} else {
@ -490,10 +438,7 @@ class BleContentTransfer: ObservableObject {
}
enum ListCommandError: Error {
case belowMinimum
case isPrime
}
func checkIfFilesExistOnBoard(url: URL) {
@ -527,29 +472,16 @@ class BleContentTransfer: ObservableObject {
var tempPathComponents = url.pathComponents
print("Incoming URL for makeFileString: \(url.absoluteString)")
if sharedBootinfo.contains("CircuitPython 7") {
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython 7.x")!
tempPathComponents.removeSubrange(0...indexOfCP)
tempPathComponents.removeLast()
var joinedArrayPath = tempPathComponents.joined(separator: "/")
print("\(#function) @Line: \(#line)")
print("Outgoing makeFileString: \(joinedArrayPath) for CP 7")
return joinedArrayPath
}
if sharedBootinfo.contains("CircuitPython 8") {
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython 8.x")!
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython \(Board.shared.versionNumber).x")!
tempPathComponents.removeSubrange(0...indexOfCP)
tempPathComponents.removeLast()
var joinedArrayPath = tempPathComponents.joined(separator: "/")
print("\(#function) @Line: \(#line)")
print("Outgoing makeFileString: \(joinedArrayPath) for CP 8")
return joinedArrayPath
}
return ""
}
@ -561,256 +493,81 @@ class BleContentTransfer: ObservableObject {
print("Incoming URL: \(url.absoluteString) ")
if sharedBootinfo.contains("CircuitPython 7") {
print(tempPathComponents)
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython 7.x")!
print(tempPathComponents)
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython \(Board.shared.versionNumber).x")!
tempPathComponents.removeSubrange(0...indexOfCP)
var joinedArrayPath = tempPathComponents.joined(separator: "/")
print("\(#function) @Line: \(#line)")
print("Outgoing String: \(joinedArrayPath) for CP 7")
print("Outgoing Directory String: \(joinedArrayPath) for CP \(Board.shared.versionNumber)")
return joinedArrayPath
}
if sharedBootinfo.contains("CircuitPython 8") {
indexOfCP = tempPathComponents.firstIndex(of: "CircuitPython 8.x")!
tempPathComponents.removeSubrange(0...indexOfCP)
var joinedArrayPath = tempPathComponents.joined(separator: "/")
print("\(#function) @Line: \(#line)")
print("Outgoing String: \(joinedArrayPath) for CP 8")
return joinedArrayPath
} else {
guard let projectName = projectName else {
return "Unknown"
}
indexOfCP = tempPathComponents.firstIndex(of: projectName)!
tempPathComponents.removeSubrange(0...indexOfCP)
var joinedArrayPath = tempPathComponents.joined(separator: "/")
print("\(#function) @Line: \(#line)")
print("Outgoing String: \(joinedArrayPath) for CP 7")
return joinedArrayPath
}
return ""
}
func newTransfer(listOf urls: [URL]) {
print("\(#function) @Line: \(#line)")
var copiedFiles = urls
print("Files left for transfer: \(urls.count)")
print("Attempting to transfer... \(urls.first?.lastPathComponent ?? "No file found")")
DispatchQueue.main.async {
self.counter += 1
}
if copiedFiles.isEmpty {
guard !urls.isEmpty else {
print("All Files Transferred! 👍")
self.completedTransfer()
completedTransfer()
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
self.sendingBundle = false
self.completedTransfer()
self.numOfFiles = 0
self.counter = 0
self.contentList.removeAll()
self.resetTransferParameters()
}
} else {
guard let data = try? Data(contentsOf: URL(string: copiedFiles.first!.absoluteString)!) else {
print("File not found")
return
}
if copiedFiles.first?.lastPathComponent == "code.py" || copiedFiles.first?.lastPathComponent == "README.txt" {
print("Input for writeFileCommand: \(copiedFiles.first?.absoluteString)")
self.contentCommands.writeFileCommand(path: copiedFiles.first!.lastPathComponent, data: data) { result in
switch result {
case .success(_):
print("Success ✅")
copiedFiles.removeFirst()
self.newTransfer(listOf: copiedFiles)
case .failure(let error):
print("\(#function) @Line: \(#line)")
print(error)
DispatchQueue.main.async {
print("Transfer Failure \(error)")
self.downloadState = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.downloadState = .idle
}
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
}
} else {
print("Checking... 🫥")
// print("makeDirectoryString transfer \(makeDirectoryString(url: copiedFiles.first!))")
let directoryPath = makeDirectoryString(url: copiedFiles.first!)
print("Input writeFileCommand: \(directoryPath)")
self.contentCommands.writeFileCommand(path: directoryPath, data: data) { result in
switch result {
case .success(_):
print("Success ✅")
copiedFiles.removeFirst()
self.newTransfer(listOf: copiedFiles)
case .failure(let error):
print("\(#function) @Line: \(#line)")
print(error)
DispatchQueue.main.async {
print("Transfer Failure \(error)")
self.downloadState = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.downloadState = .idle
}
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
}
}
return
}
print("\(#function) @Line: \(#line)")
print("Files left for transfer: \(urls.count)")
print("Attempting to transfer... \(urls.first?.lastPathComponent ?? "No file found")")
DispatchQueue.main.async { self.counter += 1 }
guard let url = urls.first,
let data = try? Data(contentsOf: url) else {
print("File not found")
return
}
let path = isDirectFile(name: url.lastPathComponent) ? url.lastPathComponent : makeDirectoryString(url: url)
contentCommands.writeFileCommand(path: path, data: data) { result in
switch result {
case .success:
print("Success ✅")
self.newTransfer(listOf: Array(urls.dropFirst()))
case .failure(let error):
print("Transfer Failure \(error)")
self.handleTransferError(error)
}
}
}
func filePathMod(listOf files: [URL]) {
var indexOfCP = 0
for i in files {
print("-\(i.absoluteString)-\n")
}
var tempArray = files
if files.isEmpty {
// Continue
private func handleTransferError(_ error: Error) {
DispatchQueue.main.async {
print("\(#function) @Line: \(#line)")
print("Done!")
// print("RETURNED PATHS: \(returnedArray)\n")
for i in filesReadyForTransfer {
print("\(i)")
print(error)
self.downloadState = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.downloadState = .idle
}
// self.transferFiles(files: fileArray)
// validateDirectory(directoryArray: returnedArray, fileArray: fileArray)
} else {
var tempPath = files[0].pathComponents
print("temp path \(tempPath)")
if tempPath.contains("CircuitPython 7.x") {
print("\(#function) @Line: \(#line)")
indexOfCP = tempPath.firstIndex(of: "CircuitPython 7.x")!
tempPath.removeSubrange(0...indexOfCP)
print("removeSubrange temp path - \(tempPath)")
tempArray.removeFirst()
var joinedArrayPath = tempPath.joined(separator: "/")
filesReadyForTransfer.append(URL(string: joinedArrayPath)!)
filePathMod(listOf: tempArray)
}
if tempPath.contains("CircuitPython 8.x") {
print("\(#function) @Line: \(#line)")
indexOfCP = tempPath.firstIndex(of: "CircuitPython 8.x")!
tempPath.removeSubrange(0...indexOfCP)
print("removeSubrange temp path - \(tempPath)")
tempArray.removeFirst()
var joinedArrayPath = tempPath.joined(separator: "/")
filesReadyForTransfer.append(URL(string: joinedArrayPath)!)
filePathMod(listOf: tempArray)
}
if tempPath.contains(projectName ?? "unknown") {
print("\(#function) @Line: \(#line)")
indexOfCP = tempPath.firstIndex(of: projectName!)!
tempPath.removeSubrange(0...indexOfCP+1)
print("removeSubrange temp path - \(tempPath)")
tempArray.removeFirst()
var joinedArrayPath = tempPath.joined(separator: "/")
filesReadyForTransfer.append(URL(string: joinedArrayPath)!)
filePathMod(listOf: tempArray)
}
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
private func resetTransferParameters() {
sendingBundle = false
completedTransfer()
numOfFiles = 0
counter = 0
contentList.removeAll()
}
private func isDirectFile(name: String) -> Bool {
return name == "code.py" || name == "README.txt"
}
func completedTransfer() {
DispatchQueue.main.async {
self.downloadState = .complete
self.didCompleteTranfer = true
@ -824,186 +581,7 @@ class BleContentTransfer: ObservableObject {
}
}
func transferFiles(files: [URL]) {
print(#function)
var copiedFiles = files
print("Number of files in filesArray \(files.count)")
print(files)
if files.isEmpty {
print("Array of contents empty - Check other directories")
self.completedTransfer()
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
self.sendingBundle = false
self.completedTransfer()
self.numOfFiles = 0
self.contentList.removeAll()
}
} else {
guard let selectedUrl = files.first else {
print("No such file exist here")
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: selectedUrl.deletingPathExtension().lastPathComponent, relativeTo: selectedUrl).appendingPathExtension(selectedUrl.pathExtension)) else {
print("File not found")
return
}
if selectedUrl.deletingLastPathComponent().lastPathComponent == "CircuitPython 7.x"{
print("Selected Path: \(selectedUrl.path)")
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
var newModPath = tempURL
newModPath.removeLast()
print("Test file path: \(tempURL)")
print("File transfer modified path xx: \(joined)")
self.contentCommands.writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(_):
DispatchQueue.main.async {
print("Transfer Failure")
print("\(joined)")
self.downloadState = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.downloadState = .idle
}
}
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
}
else if selectedUrl.deletingLastPathComponent().lastPathComponent == "lib" {
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("File transfer modified path 11:\(joined)")
print("Updated Path:\(joined)")
contentCommands.writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(_):
print("Transfer Failure - 2")
self.downloadState = .failed
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
} else {
if selectedUrl.lastPathComponent == "README.txt" {
print("Got one")
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
} else {
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("File transfer modified path: \(joined)")
print("Updated Path:\(joined)")
contentCommands.writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(let error):
print("Failed: \(error): \(result)")
NotificationCenter.default.post(name: .didEncounterTransferError, object: nil, userInfo: nil)
}
}
}
}
}
DispatchQueue.main.async {
self.sendingBundle = true
}
}
func readMyStatus() {
///model.readFile(filename: "boot_out.txt")
print(#function)
print("BOOT INFO: \(bootUpInfo)")
switch bootUpInfo.description {
case let str where str.contains("CircuitPython 7"):
print("CircuitPython 7")
circuitPythonVersion = "CircuitPython 7"
case let str where str.contains("CircuitPython 8"):
print("CircuitPython 8")
circuitPythonVersion = "CircuitPython 8"
default:
print("Unknown Device")
}
}
// MARK: System
struct TransmissionProgress {
@ -1047,7 +625,6 @@ class BleContentTransfer: ObservableObject {
@Published var lastTransmit: TransmissionLog? = TransmissionLog(type: .write(size: 334))
@Published var activeAlert: ActiveAlert?
// Data
private let bleManager = BleManager.shared
@ -1088,8 +665,7 @@ class BleContentTransfer: ObservableObject {
let str = String(decoding: data, as: UTF8.self)
print("\(#function) @Line: \(#line)")
print("Read: \(str)")
//self.readMyStatus()
self.bootUpInfo = str

View file

@ -12,28 +12,40 @@ class ExpandedBLECellState: ObservableObject {
@Published var currentCell = ""
}
class Board: Equatable {
var name: String
var versionNumber: String
static let shared = Board(name: "Unrecognized Board", versionNumber: "8")
static func == (lhs: Board, rhs: Board) -> Bool {
return lhs.name == rhs.name && lhs.versionNumber == rhs.versionNumber
}
private init(name: String, versionNumber: String) {
self.name = name
self.versionNumber = versionNumber
}
}
struct BleModuleView: View {
// Data
enum ActiveAlert: Identifiable {
case confirmUnpair(blePeripheral: BlePeripheral)
var id: Int {
switch self {
case .confirmUnpair: return 1
}
}
}
@State var boardInfoForView: Board?
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var expandedState : ExpandedBLECellState
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
@State var unknownBoardName: String?
let selectedPeripheral = FileTransferConnectionManager.shared.selectedPeripheral
@StateObject var viewModel = BleModuleViewModel()
@StateObject var vm = BleModuleViewModel()
@EnvironmentObject var rootViewModel: RootViewModel
@ -44,7 +56,6 @@ struct BleModuleView: View {
@State var isExpanded = true
@State private var scrollViewID = UUID()
@State private var activeAlert: ActiveAlert?
@State private var boardBootInfo = ""
@State private var inConnectedInSelectionView = true
@ -97,8 +108,19 @@ struct BleModuleView: View {
.minimumScaleFactor(0.01)
.multilineTextAlignment(.center)
.lineLimit(0)
.onAppear() {
print("Board Boot Information")
print(boardBootInfo)
}
Button(action: {
vm.readFile(filename: "boot_out.txt")
}, label: {
Text("Read Button")
})
.padding()
.background()
if boardBootInfo == "circuitplayground_bluefruit" {
@ -118,12 +140,7 @@ struct BleModuleView: View {
.lineLimit(2)
} else {
}
if boardBootInfo == "clue_nrf52840_express" {
} else if boardBootInfo == "clue_nrf52840_express" {
Image("clue")
.resizable()
.aspectRatio(contentMode: .fit)
@ -138,6 +155,20 @@ struct BleModuleView: View {
.lineLimit(2)
.padding(.horizontal, 20)
} else {
Image("Placeholder Board Image")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.padding(.horizontal, 60)
.fixedSize(horizontal: false, vertical: true)
Text(unknownBoardName ?? "")
.font(Font.custom("ReadexPro-Regular", size: 30))
.minimumScaleFactor(0.01)
.multilineTextAlignment(.center)
.lineLimit(2)
.padding(.horizontal, 20)
}
@ -170,89 +201,14 @@ struct BleModuleView: View {
HeaderView()
VStack {
BleBannerView(deviceName: boardInfoForView?.name ?? "Unknown Device", disconnectAction: {
showConfirmationPrompt()
})
if boardBootInfo == "circuitplayground_bluefruit" {
HStack {
Image("bluetoothLogo")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
Text("Circuit Playground Bluefruit.")
.font(Font.custom("ReadexPro-Regular", size: 14))
.minimumScaleFactor(0.1)
Button {
showConfirmationPrompt()
} label: {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
.minimumScaleFactor(0.1)
}
}
}
if boardBootInfo == "clue_nrf52840_express" {
VStack {
HStack {
Image("bluetoothLogo")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
Text("Adafruit CLUE.")
.font(Font.custom("ReadexPro-Regular", size: 14))
Button {
showConfirmationPrompt()
} label: {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
//.minimumScaleFactor(0.1)
}
}
// Expandable
VStack {
Text("More Info")
}
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
.onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
.frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
.clipped()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.onTapGesture {
withAnimation(.easeIn(duration: 0.5)) {
isExpanded.toggle()
}
}
}
}
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
@ -270,14 +226,32 @@ struct BleModuleView: View {
MainSubHeaderView(device: "Adafruit CLUE")
}
if boardBootInfo == "circuitplayground_bluefruit" {
else if boardInfoForView?.name == "Circuitplayground Bluefruit" {
MainSubHeaderView(device: "Circuit Playground")
}
else {
MainSubHeaderView(device: unknownBoardName ?? "device")
}
let check = viewModel.pdemos.filter {
$0.compatibility.contains(boardBootInfo)
let check = vm.pdemos.filter {
if boardInfoForView?.name == "Circuitplayground Bluefruit" {
let cpbProjects = $0.compatibility.contains("circuitplayground_bluefruit")
print("Returned \(cpbProjects) for circuitplayground_bluefruit")
return cpbProjects
}
else if boardInfoForView?.name == "Clue Nrf52840 Express" {
let clueProjects = $0.compatibility.contains("clue_nrf52840_express")
return clueProjects
}
else {
return true
}
}
@ -311,6 +285,9 @@ struct BleModuleView: View {
.id(self.scrollViewID)
}
.environmentObject(expandedState)
.refreshable {
vm.fetchAndLoadProjectsFromStorage()
}
}
}
@ -318,50 +295,35 @@ struct BleModuleView: View {
.background(Color.white)
.onChange(of: viewModel.bootUpInfo, perform: { newValue in
viewModel.readMyStatus()
.onChange(of: vm.bootUpInfo, perform: { newValue in
vm.readMyStatus()
print("newValue \(newValue)")
boardBootInfo = newValue
})
.onChange(of: connectionManager.selectedClient) { selectedClient in
viewModel.setup(fileTransferClient: selectedClient)
vm.setup(fileTransferClient: selectedClient)
}
.onChange(of: vm.connectedBoard, perform: { newValue in
dump(newValue)
boardInfoForView = newValue
unknownBoardName = newValue?.name
})
.onAppear(){
print("Opened BleModuleView")
// networkServiceModel.fetch()
viewModel.setup(fileTransferClient:connectionManager.selectedClient)
vm.setup(fileTransferClient:connectionManager.selectedClient)
connectionManager.isSelectedPeripheralReconnecting = true
vm.readFile(filename: "boot_out.txt")
print("x\(boardBootInfo)")
viewModel.readFile(filename: "boot_out.txt")
}
}
struct Alerts: ViewModifier {
@Binding var activeAlert: ActiveAlert?
func body(content: Content) -> some View {
content
.alert(item: $activeAlert, content: { alert in
switch alert {
case .confirmUnpair(let blePeripheral):
return Alert(
title: Text("Confirm disconnect \(blePeripheral.name ?? "")"),
message: nil,
primaryButton: .destructive(Text("Disconnect")) {
//BleAutoReconnect.clearAutoconnectPeripheral()
BleManager.shared.disconnect(from: blePeripheral)
},
secondaryButton: .cancel(Text("Cancel")) {})
}
})
}
}
}
struct ViewHeightKey: PreferenceKey {

View file

@ -10,30 +10,61 @@ import Zip
import FileTransferClient
class BleModuleViewModel: ObservableObject {
weak var delegate: BoardInfoDelegate?
@Published var boardInfoForView: Board? {
didSet {
print("Changed")
delegate?.boardInfoDidUpdate(to: boardInfoForView)
}
}
private weak var fileTransferClient: FileTransferClient?
@StateObject var contentTransfer = BleContentTransfer()
@State var contentTransfer = BleContentTransfer()
@Published var entries = [BlePeripheral.DirectoryEntry]()
@Published var isTransmiting = false
@Published var bootUpInfo = ""
@Published var isTransmitting = false
var boardDataProvider = BoardDataProvider()
var connectedBoard: Board?
let dataStore = DataStore()
@ObservedObject var networkModel = NetworkService()
@Published var pdemos : [ResultItem] = []
@Published var pdemos : [PyProject] = []
init() {
pdemos = dataStore.loadDefaultList()
func loadProjectsFromStorage() {
self.pdemos = self.dataStore.loadDefaultList()
}
func fetchAndLoadProjectsFromStorage() {
self.networkModel.fetch {
self.pdemos = self.dataStore.loadDefaultList()
}
}
init() {
loadProjectsFromStorage()
self.delegate = contentTransfer
setup(fileTransferClient: fileTransferClient)
}
enum ProjectViewError: LocalizedError {
case fileTransferUndefined
}
func readMyStatus() {
print("BOOT INFO: \(bootUpInfo)")
switch bootUpInfo.description {
@ -41,11 +72,11 @@ class BleModuleViewModel: ObservableObject {
case let str where str.contains("circuitplayground_bluefruit"):
print("Circuit Playground Bluefruit device")
bootUpInfo = "circuitplayground_bluefruit"
case let str where str.contains("clue_nrf52840_express"):
print("Clue device")
bootUpInfo = "clue_nrf52840_express"
default:
print("Unknown Device")
}
@ -71,7 +102,6 @@ class BleModuleViewModel: ObservableObject {
@Published var transmissionProgress: TransmissionProgress?
@Published var lastTransmit: TransmissionLog? = TransmissionLog(type: .write(size: 334))
@Published var activeAlert: ActiveAlert?
// Data
private let bleManager = BleManager.shared
@ -100,7 +130,9 @@ class BleModuleViewModel: ObservableObject {
return modeText
}
}
// MARK: - Setup
func onAppear() {
//registerNotifications(enabled: true)
@ -121,6 +153,28 @@ class BleModuleViewModel: ObservableObject {
}
func setupBoardInfoForDisplay(_ boardInfo: String?) -> Board {
// First, safely unwrap the optional 'boardInfo' string
guard let info = boardInfo else {
print("boardInfo is nil")
return Board.shared
}
let boardID = boardDataProvider.getBoardID(from: info) ?? "Unrecognized Board"
// Board default version is set to 8
let boardVersion = boardDataProvider.getCircuitPythonMajorVersion(from: info) ?? "8"
Board.shared.name = boardID
Board.shared.versionNumber = boardVersion
// Create a new Board object with the acquired name and version
let board = Board.shared
return board
}
// MARK: - Actions
func readFile(filename: String) {
@ -135,10 +189,13 @@ class BleModuleViewModel: ObservableObject {
let str = String(decoding: data, as: UTF8.self)
print("Read: \(str)")
self.bootUpInfo = str
sharedBootinfo = str
self.connectedBoard = self.setupBoardInfoForDisplay(str)
self.boardInfoForView = self.connectedBoard
case .failure(let error):
print("Error: \(error.localizedDescription)")
self.lastTransmit = TransmissionLog(type: .error(message: error.localizedDescription))
}
@ -147,6 +204,45 @@ class BleModuleViewModel: ObservableObject {
}
}
func readFile(filePath: String) {
readFile(filePath: filePath, fileTransferClient: fileTransferClient!) { data in
print(data)
}
}
func readFile(filePath: String, fileTransferClient: FileTransferClient, completion: ((Result<Data, Error>) -> Void)? = nil) {
startCommand(description: "Reading \(filePath)")
isTransmitting = true
fileTransferClient.readFile(path: filePath, progress: { [weak self] read, total in
//DLog("reading progress: \( String(format: "%.1f%%", Float(read) * 100 / Float(total)) )")
guard let self = self else { return }
DispatchQueue.main.async {
self.transmissionProgress?.transmittedBytes = read
self.transmissionProgress?.totalBytes = total
}
}) { [weak self] result in
guard let self = self else { return }
DispatchQueue.main.async {
self.isTransmitting = false
switch result {
case .success(let data):
self.lastTransmit = TransmissionLog(type: .read(data: data))
completion?(.success(data))
case .failure(let error):
DLog("readFile \(filePath) error: \(error)")
self.lastTransmit = TransmissionLog(type: .error(message: error.localizedDescription))
completion?(.failure(error))
}
self.endCommand()
}
}
}
func writeFile(filename: String, data: Data) {
startCommand(description: "Writing \(filename)")
writeFileCommand(path: filename, data: data) { [weak self] result in
@ -344,18 +440,4 @@ class BleModuleViewModel: ObservableObject {
completion?(result)
}
}
}
public var sharedBootinfo = ""
enum ActiveAlert: Identifiable {
case error(error: Error)
var id: Int {
switch self {
case .error: return 1
}
}
}

View file

@ -10,13 +10,13 @@ import FileTransferClient
struct DemoSubview: View {
@Binding var bindingString: String
let result: ResultItem
let result: PyProject
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = SubCellViewModel()
@StateObject var contentTransfer = BleContentTransfer()
//@StateObject var contentTransfer = BleContentTransfer()
@StateObject var contentTransfer = BleContentTransfer.shared
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
@ -150,16 +150,7 @@ Try again later
if isConnected {
if result.compatibility.contains(bindingString) {
// Button {
// viewModel.deleteStoredFilesInFM()
// } label: {
// Text("Delete File Manager Contents")
// .bold()
// .padding(12)
// }
if contentTransfer.downloadState == .idle {
Button(action: {
@ -210,7 +201,7 @@ Try again later
CompleteButton()
.padding(.top, 20)
}
}
} else {
@ -231,9 +222,7 @@ Try again later
print("On Appear")
contentTransfer.contentCommands.setup(fileTransferClient: connectionManager.selectedClient)
// viewModel.readFile(filename: "boot_out.txt")
}
}
.onChange(of: contentTransfer.transferError, perform: { newValue in
if newValue {
@ -246,15 +235,9 @@ Try again later
showDownloadErrorMessage()
}
})
// .onChange(of: connectionManager.selectedClient) { selectedClient in
// viewModel.setup(fileTransferClient: selectedClient)
// }
.onAppear(perform: {
contentTransfer.readMyStatus()
viewModel.searchPathForProject(nameOf: result.projectName)
if viewModel.projectDownloaded {

View file

@ -11,7 +11,7 @@ struct DemoViewCell: View {
@EnvironmentObject var expandedState : ExpandedBLECellState
let result : ResultItem
let result : PyProject
@State var isExpanded: Bool = false {
didSet {
@ -36,7 +36,7 @@ struct DemoViewCell: View {
if isExpanded {
Group {
DemoSubview(bindingString: $deviceInfo, result: result, isConnected: $isConnected)
DemoSubview(result: result, isConnected: $isConnected)
}
}

View file

@ -54,8 +54,7 @@ struct RootView: View {
case .selection:
SelectionView()
case .wifiSelection:
WifiSelection()
case .wifiPairingTutorial:
WifiPairingView()

View file

@ -47,7 +47,7 @@ public class RootViewModel: ObservableObject {
}
func goToWiFiSelection() {
destination = .wifiSelection
destination = .wifiServiceSelection
}
func goToWifiView() {

View file

@ -16,8 +16,6 @@ struct WifiHeaderView: View {
VStack {
HStack (alignment: .center, spacing: 0) {
Image(systemName: "gearshape")

View file

@ -0,0 +1,143 @@
//
// MainSelectionView.swift
// PyLeap
//
// Created by Trevor Beaton on 10/16/21.
//
import SwiftUI
import FileTransferClient
enum AdafruitDevices {
case clue_nrf52840_express
case circuitplayground_bluefruit
case esp32s2
}
struct MainSelectionView: View {
@State private var showWebViewPopover: Bool = false
@State private var inConnectedInSelectionView = false
@State private var boardBootInfo = ""
@EnvironmentObject var expandedState : ExpandedBLECellState
@ObservedObject var vm = MainSelectionViewModel()
@State private var isConnected = false
@State private var test = ""
@State private var nilBinder = DownloadState.idle
@EnvironmentObject var rootViewModel: RootViewModel
@AppStorage("shouldShowOnboarding") var shouldShowOnboarding: Bool = true
var body: some View {
VStack(alignment: .center, spacing: 0) {
MainHeaderView()
HStack(alignment: .center, spacing: 8, content: {
Text("Not Connected to a Device.")
.font(Font.custom("ReadexPro-Regular", size: 16))
Button {
rootViewModel.goToSelection()
} label: {
Text("Connect Now")
.font(Font.custom("ReadexPro-Regular", size: 16))
.underline()
}
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
.frame(maxHeight: 40)
.background(Color("pyleap_burg"))
.foregroundColor(.white)
ScrollView {
MainSubHeaderView(device: "Adafruit device")
if vm.pdemos.isEmpty {
HStack{
Spacer()
ProgressView()
.scaleEffect(2)
Spacer()
}
.padding(0)
}
ScrollViewReader { scroll in
ForEach(vm.pdemos) { demo in
if demo.bundleLink == expandedState.currentCell {
DemoViewCell(result: demo, isExpanded: true, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
.onAppear(){
print("Cell Appeared")
withAnimation {
scroll.scrollTo(demo.id)
}
}
} else {
DemoViewCell(result: demo, isExpanded: false, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
}
}
}
}
.refreshable {
vm.pdemos = []
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
vm.networkModel.fetch {
vm.loadProjectsFromStorage()
}
}
}
}
.onAppear() {
print("Opened MainSelectionView")
}
.fullScreenCover(isPresented: $shouldShowOnboarding, content: {
ExampleView(shouldShowOnboarding: $shouldShowOnboarding)
})
.preferredColorScheme(.light)
.background(Color.white)
.navigationBarColor(UIColor(named: "pyleap_gray"))
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -17,23 +17,23 @@ class InternetConnectionManager: ObservableObject {
@Published var isConnected = false
init() {
startMonitoring(completion: {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
let newIsConnected = path.status == .satisfied
if self.isConnected != newIsConnected {
self.isConnected = newIsConnected
print("net: \(path.status) \(self.isConnected)")
}
if self.isConnected != newIsConnected {
self.isConnected = newIsConnected
print("net: \(path.status) \(self.isConnected)")
}
}
}
})
}
func startMonitoring(completion:()->Void) {
print("Start Monitoring Network")
print("Start Monitoring Network")
monitor.start(queue: queue)
completion()
@ -57,32 +57,43 @@ class MainSelectionViewModel: ObservableObject {
let dataStore = DataStore()
@Published var pdemos : [ResultItem] = []
@Published var pdemos: [PyProject] = []
var networkMonitorCancellable: AnyCancellable?
init() {
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
networkMonitorCancellable = networkMonitor.$isConnected.sink { isConnected in
if isConnected {
print("The device is currently connected to the internet.")
// Perform some action when the device is connected to the internet.
self.networkModel.fetch {
self.pdemos = self.dataStore.loadDefaultList()
}
} else {
print("The device is not currently connected to the internet.")
// Perform some action when the device is not connected to the internet.
print("Loading cached remote data.")
self.pdemos = self.dataStore.loadDefaultList()
}
}
startUp()
}
func loadProjectsFromStorage() {
self.pdemos = self.dataStore.loadDefaultList()
}
func fetchAndLoadProjectsFromStorage() {
self.networkModel.fetch {
self.pdemos = self.dataStore.loadDefaultList()
}
}
func startUp(){
networkMonitorCancellable = networkMonitor.$isConnected.sink { isConnected in
if isConnected {
// Perform some action when the device is connected to the internet.
self.fetchAndLoadProjectsFromStorage()
print("The device is currently connected to the internet.")
} else {
print("The device is not currently connected to the internet.")
// Perform some action when the device is not connected to the internet.
print("Loading cached remote data.")
self.loadProjectsFromStorage()
}
}
}
}

View file

@ -54,7 +54,7 @@ struct SelectionView: View {
Button {
rootViewModel.goToWiFiSelection()
} label: {
Text("Wifi")
Text("WiFi")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
@ -110,6 +110,9 @@ struct SelectionView: View {
}
.padding(.bottom, 60)
.onAppear() {
print("In Selection view")
}
}
}

View file

@ -19,7 +19,7 @@ struct WifiCell: View {
@EnvironmentObject var expandedState : ExpandedState
let result : ResultItem
let result : PyProject
@State var isExpanded: Bool = false {
didSet {

View file

@ -15,7 +15,7 @@ struct WifiSubViewCell: View {
@StateObject var wifiFileTransfer = WifiFileTransfer()
@StateObject var wifiTransferService = WifiTransferService()
let result : ResultItem
let result : PyProject
@Binding var bindingString: String
@ -85,7 +85,7 @@ Remove device from USB. Press "Reset" on the device.
func testOperation() {
func startOperationQueue() {
let operationQueue = OperationQueue()
let operation1 = BlockOperation {
@ -157,49 +157,18 @@ Remove device from USB. Press "Reset" on the device.
Text("Compatible with:")
.font(Font.custom("ReadexPro-Bold", size: 18))
.padding(.top, 5)
HStack {
Image(systemName: "checkmark")
.resizable()
.frame(width: 25, height: 22, alignment: .center)
.foregroundColor(.green)
Text("ESP32-S2")
.font(Font.custom("ReadexPro-Regular", size: 18))
.foregroundColor(.black)
}
.padding(.top, 10)
ForEach(result.compatibility, id: \.self) { string in
if string == "circuitplayground_bluefruit" {
HStack {
Image(systemName: "checkmark")
.resizable()
.frame(width: 25, height: 22, alignment: .center)
.foregroundColor(.green)
Text("Circuit Playground Bluefruit")
.font(Font.custom("ReadexPro-Regular", size: 18))
.foregroundColor(.black)
}
.padding(.top, 10)
}
if string == "clue_nrf52840_express" {
HStack {
Image(systemName: "checkmark")
.resizable()
.frame(width: 25, height: 22, alignment: .center)
.foregroundColor(.green)
Text("Adafruit CLUE")
.font(Font.custom("ReadexPro-Regular", size: 18))
.foregroundColor(.black)
}
.padding(.top, 10)
ForEach(result.compatibility, id: \.self) { device in
HStack {
Image(systemName: "checkmark")
.resizable()
.frame(width: 25, height: 22, alignment: .center)
.foregroundColor(.green)
Text(formatDeviceName(device))
.font(Font.custom("ReadexPro-Regular", size: 18))
.foregroundColor(.black)
}
.padding(.top, 10)
}
})
.ignoresSafeArea(.all)
@ -219,18 +188,15 @@ Remove device from USB. Press "Reset" on the device.
if isConnected {
if result.compatibility.contains(bindingString) {
if wifiFileTransfer.testIndex.downloadState == .idle {
Button {
// NotificationCenter.default.post(name: .didCompleteZip, object: nil, userInfo: projectResponse)
testOperation()
startOperationQueue()
} label: {
RunItButton()
@ -270,20 +236,8 @@ Remove device from USB. Press "Reset" on the device.
.disabled(true)
}
}
} else {
Button {
rootViewModel.goTobluetoothPairing()
} label: {
ConnectButton()
.padding(.top, 20)
}
}
}
Spacer()

View file

@ -11,7 +11,7 @@ import Combine
struct WifiView: View {
@StateObject var viewModel = WifiViewModel()
@StateObject var vm = WifiViewModel()
private let kPrefix = Bundle.main.bundleIdentifier!
// User Defaults
@ -38,12 +38,12 @@ struct WifiView: View {
@State private var showPopover: Bool = false
func toggleViewModelIP() {
viewModel.isInvalidIP.toggle()
vm.isInvalidIP.toggle()
}
func scanNetworkWifi() {
viewModel.wifiServiceManager.findService()
vm.wifiServiceManager.findService()
}
func printArray(array: [Any]) {
@ -59,9 +59,9 @@ struct WifiView: View {
} else {
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
viewModel.ipAddressStored = true
vm.ipAddressStored = true
print("storeResolvedAddress - is stored")
viewModel.connectionStatus = .connected
vm.connectionStatus = .connected
}
}
@ -71,28 +71,40 @@ struct WifiView: View {
hintText: "IP Address...",
primaryTitle: "Done",
secondaryTitle: "Cancel") { text in
viewModel.checkServices(ip: text)
vm.checkServices(ip: text)
} secondaryAction: {
print("Cancel")
}
}
func showConfirmationPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
rootViewModel.goToSelection()
} cancel: {
}
}
func showAlertMessage() {
alertMessage(title: "IP address Not Found", exitTitle: "Ok") {
showValidationPrompt()
}
}
@State var boardInfoForView = Board.shared
var body: some View {
VStack(spacing: 0) {
WifiHeaderView()
Group{
switch viewModel.connectionStatus {
switch vm.connectionStatus {
case .connected:
WifiStatusConnectedView(hostName: $hostName)
WifiStatusConnectedView(hostName: $hostName, disconnectAction: {
showConfirmationPrompt()
})
case .noConnection:
WifiStatusNoConnectionView()
case .connecting:
@ -100,16 +112,14 @@ struct WifiView: View {
}
}
ScrollView(.vertical, showsIndicators: false) {
ScrollViewReader { scroll in
SubHeaderView()
let check = viewModel.pdemos.filter {
$0.compatibility.contains(boardBootInfo)
let check = vm.pdemos.filter {
$0.wifiCompatible
}
ForEach(check) { demo in
@ -117,48 +127,43 @@ struct WifiView: View {
if demo.bundleLink == test.currentCell {
WifiCell(result: demo,isExpanded: trueTog, isConnected: $inConnectedInWifiView, bootOne: $boardBootInfo, stateBinder: $downloadState, onViewGeometryChanged: {
})
.onAppear(){
withAnimation {
scroll.scrollTo(demo.id)
}
}
} else {
WifiCell(result: demo, isExpanded: falseTog, isConnected: $inConnectedInWifiView, bootOne: $boardBootInfo, stateBinder: $downloadState, onViewGeometryChanged: {
withAnimation {
// scroll.scrollTo(demo.id)
}
})
}
}
}
.id(self.scrollViewID)
}
.foregroundColor(.black)
.environmentObject(test)
.refreshable {
vm.fetchAndLoadProjectsFromStorage()
}
}
.onChange(of: viewModel.connectionStatus, perform: { newValue in
.onChange(of: vm.connectionStatus, perform: { newValue in
if newValue == .connected {
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
}
})
.onChange(of: viewModel.wifiServiceManager.resolvedServices, perform: { newValue in
.onChange(of: vm.wifiServiceManager.resolvedServices, perform: { newValue in
print("Credential Check!")
print(newValue)
@ -173,7 +178,7 @@ struct WifiView: View {
})
.onChange(of: viewModel.isInvalidIP, perform: { newValue in
.onChange(of: vm.isInvalidIP, perform: { newValue in
print("viewModel.isInvalidIP .onChange")
if newValue {
showAlertMessage()
@ -185,8 +190,8 @@ struct WifiView: View {
.onAppear(){
checkForStoredIPAddress()
viewModel.printStoredInfo()
viewModel.read()
vm.printStoredInfo()
vm.read()
}
}

View file

@ -17,52 +17,7 @@ struct TestIndex {
}
class WifiFileTransfer: ObservableObject {
// func fetchDocuments<T: Sequence>(in sequence: T) where T.Element == Int {
// var documentNumbers = sequence.map { String($0) }
//
// let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
// guard
// let self = self,
// let documentNumber = documentNumbers.first
// else {
// timer.invalidate()
// return
// }
//
// self.fetchDocument(byNumber: documentNumber)
// documentNumbers.removeLast()
// }
// timer.fire() // if you don't want to wait 2 seconds for the first one to fire, go ahead and fire it manually
// }
func fetchDocumentsq<T: Sequence>(in sequence: T) where T.Element == URL {
print(sequence)
guard let value = sequence.first(where: { _ in true }) else {
print("Complete - fetchDocumentsq")
return
}
// let docNumber = String(value)
// fetchDocument(byNumber: docNumber)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
// self?.fetchDocuments(in: sequence.dropFirst())
print(value)
self?.fetchDocumentsq(in: sequence.dropFirst())
}
}
struct TestIndex {
var count = 0
var numberOfFiles = 0
@ -74,13 +29,10 @@ class WifiFileTransfer: ObservableObject {
downloadState = .idle
}
}
var testIndex = TestIndex()
func copy(with zone: NSZone? = nil) -> Any {
let copy = WifiFileTransfer()
copy.counter = counter
@ -125,6 +77,7 @@ class WifiFileTransfer: ObservableObject {
func printArray(array: [Any]) {
print("From print array:")
for i in array {
print("\(i)")
}
@ -339,34 +292,20 @@ class WifiFileTransfer: ObservableObject {
func filterOutCPDirectories(urls: [URL]) -> [URL] {
// Removes - CircuitPython 8.x directory at the lastPathComponent
let removingCP8FromArray = urls.filter {
$0.lastPathComponent != ("CircuitPython 8.x")
let filteredList = urls.filter {
let lastPathComponent = $0.lastPathComponent
return lastPathComponent != "CircuitPython 8.x"
&& lastPathComponent != "CircuitPython 7.x"
&& lastPathComponent != "CircuitPython_Templates"
}
// Removes - CircuitPython 7.x directory at the lastPathComponent
let removingCP7FromArray = removingCP8FromArray.filter {
$0.lastPathComponent != ("CircuitPython 7.x")
let listForCurrentCPVersion = filteredList.filter {
$0.absoluteString.contains("CircuitPython%20\(Board.shared.versionNumber).x")
}
if WifiCPVersion.versionNumber == 8 {
let listForCurrentCPVersion = removingCP7FromArray.filter {
!$0.absoluteString.contains("CircuitPython%207.x")
}
return listForCurrentCPVersion
}
printArray(array: listForCurrentCPVersion)
if WifiCPVersion.versionNumber == 7 {
let listForCurrentCPVersion = removingCP7FromArray.filter {
!$0.absoluteString.contains("CircuitPython%208.x")
}
return listForCurrentCPVersion
}
return removingCP7FromArray
return listForCurrentCPVersion
}
@ -419,8 +358,6 @@ class WifiFileTransfer: ObservableObject {
DispatchQueue.main.async {
self.numOfFiles = tempArray.count
self.makeFile(files: tempArray)
// self.testIndex.numberOfFiles = self.numOfFiles
}
@ -618,7 +555,6 @@ class WifiFileTransfer: ObservableObject {
printArray(array: files)
var copiedArray = files
// self.fetchDocumentsq(in: files)
DispatchQueue.main.async {
self.counter += 1

View file

@ -0,0 +1,99 @@
//
// WifiServiceCellSubView.swift
// PyLeap
//
// Created by Trevor Beaton on 3/29/23.
//
import SwiftUI
import Foundation
struct WifiServiceCellSubView: View {
let resolvedService: ResolvedService
@EnvironmentObject var rootViewModel: RootViewModel
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
func storeResolvedAddress(service: ResolvedService) {
print("Storing resolved address")
userDefaults.set(service.ipAddress, forKey: kPrefix+".storeResolvedAddress.ipAddress" )
userDefaults.set(service.hostName, forKey: kPrefix+".storeResolvedAddress.hostName" )
userDefaults.set(service.device, forKey: kPrefix+".storeResolvedAddress.device" )
print("Stored UserDefaults")
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device"))
}
func showConfirmationPrompt(service: ResolvedService, hostName: String) {
comfirmationAlertMessage(title: "Would you like to connect to \(hostName)?", exitTitle: "Cancel", primaryTitle: "Connect") {
storeResolvedAddress(service: service)
rootViewModel.goToWifiView()
} cancel: {
}
}
var body: some View {
VStack {
HStack {
VStack {
Text("Device ID: \(resolvedService.hostName)")
.font(Font.custom("ReadexPro-Regular", size: 18))
.multilineTextAlignment(.leading)
.minimumScaleFactor(0.1)
Text("Device IP: \(resolvedService.ipAddress)")
.font(Font.custom("ReadexPro-Regular", size: 18))
.multilineTextAlignment(.leading)
.minimumScaleFactor(0.1)
}
Spacer()
}
.padding(.horizontal, 30)
.padding(.vertical, 30)
HStack (
alignment: .center,
spacing: 0
) {
Spacer()
Button {
showConfirmationPrompt(service: resolvedService, hostName: resolvedService.hostName)
} label: {
Text("Connect")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
.padding(.bottom, 30)
}
Spacer()
}
}
}
}
//struct WifiServiceCellSubView_Previews: PreviewProvider {
// static var previews: some View {
// WifiServiceCellSubView(resolvedService: .con)
// }
//}

View file

@ -0,0 +1,81 @@
//
// WifiServiceCellView.swift
// PyLeap
//
// Created by Trevor Beaton on 11/7/22.
//
import SwiftUI
struct WifiServiceCellView: View {
let resolvedService: ResolvedService
@State private var isExpanded: Bool = false {
didSet {
onViewGeometryChanged()
}
}
let onViewGeometryChanged: ()->Void
var body: some View {
content
.frame(maxWidth: .infinity)
}
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
WifiServiceCellSubView(resolvedService: resolvedService)
}
}
}
}
func removeAdafruitString(text: String) -> String {
if text.contains("Adafruit") {
let parsed = text.replacingOccurrences(of: "Adafruit", with: "")
return parsed
} else {
return text
}
}
private var header: some View {
HStack {
Text(removeAdafruitString(text:resolvedService.device))
.font(Font.custom("ReadexPro-Regular", size: 24))
.minimumScaleFactor(0.1)
.lineLimit(1)
// .padding(.horizontal, 30)
.foregroundColor(.white)
Spacer()
Image(systemName: "chevron.down")
.resizable()
.frame(width: 30, height: 15, alignment: .center)
.foregroundColor(.white)
.padding(.trailing, 30)
}
//.padding(.vertical, 5)
.padding(.leading)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color("alt-gray"))
.onTapGesture { isExpanded.toggle() }
}
}

View file

@ -3,7 +3,7 @@
// PyLeap
//
// Created by Trevor Beaton on 10/24/22.
//
// Testing
import SwiftUI

View file

@ -9,19 +9,16 @@ import SwiftUI
import Foundation
struct WifiStatusConnectedView: View {
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
@EnvironmentObject var rootViewModel: RootViewModel
@Binding var hostName: String
var disconnectAction: () -> Void
func showConfirmationPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
rootViewModel.goToSelection()
} cancel: {
}
func removeAdafruitString(from boardName: String) -> String {
let removeString = "Adafruit"
let updatedText = boardName.replacingOccurrences(of: removeString, with: "")
return updatedText
}
var body: some View {
@ -33,16 +30,13 @@ struct WifiStatusConnectedView: View {
.frame(width: 20, height: 20)
.padding(5)
Text("Connected to \(hostName). ")
Text("Connected to \(hostName) ")
.font(Font.custom("ReadexPro-Regular", size: 14))
Button {
showConfirmationPrompt()
} label: {
Button(action: disconnectAction) {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
.minimumScaleFactor(0.1)
}
})

View file

@ -69,32 +69,7 @@ class WifiSubViewCellModel: ObservableObject {
print("Failure")
}
// if success.contains("GET, OPTIONS, PUT, DELETE, MOVE") {
//
// print("USB not in use.")
// DispatchQueue.main.async {
// self.usbInUse = false
// }
//
// // DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// // if wifiFileTransfer.projectDownloaded {
// //
// // wifiFileTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink)
// //
// // } else {
// // downloadModel.trueDownload(useProject: result.bundleLink, projectName: result.projectName)
// // }
// // }
//
// } else {
// DispatchQueue.main.async {
// self.usbInUse = true
// }
// print("USB in use - files cannot be tranferred or moved while USB is in use. Show Error")
// }
})

View file

@ -173,7 +173,7 @@ class WifiTransferService: ObservableObject {
}
let base64LoginString = loginData.base64EncodedString()
// var request = URLRequest(url: URL(string: "http://cpy-9cbe10.local/fs/")!,timeoutInterval: Double.infinity)
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
@ -226,7 +226,6 @@ class WifiTransferService: ObservableObject {
let loginData = loginString.data(using: String.Encoding.utf8)
let base64LoginString = loginData!.base64EncodedString()
print("Host Name: \(hostName)")

View file

@ -27,6 +27,7 @@ class WifiViewModel: ObservableObject {
@Published var ipInputValidation = false
//Dependencies
var networkMonitor = NetworkMonitor()
var networkAuth = LocalNetworkAuthorization()
public var wifiNetworkService = WifiNetworkService()
@ -34,7 +35,7 @@ class WifiViewModel: ObservableObject {
@Published var wifiTransferService = WifiTransferService()
@Published var wifiServiceManager = WifiServiceManager()
@ObservedObject var networkModel = NetworkService()
var circuitPythonVersion = Int()
@Published var webDirectoryInfo = [WebDirectoryModel]()
@ -45,9 +46,19 @@ class WifiViewModel: ObservableObject {
let dataStore = DataStore()
@Published var pdemos : [ResultItem] = []
@Published var pdemos : [PyProject] = []
func loadProjectsFromStorage() {
self.pdemos = self.dataStore.loadDefaultList()
}
func fetchAndLoadProjectsFromStorage() {
self.networkModel.fetch {
self.pdemos = self.dataStore.loadDefaultList()
}
}
// File Manager Data
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
@ -59,11 +70,10 @@ class WifiViewModel: ObservableObject {
var ipAddressStored = false
init() {
pdemos = dataStore.loadDefaultList()
loadProjectsFromStorage()
checkIP()
registerNotifications(enabled: true)
wifiServiceManager.findService()
//read()
}
/// Makes a network call to populate our project list
@ -71,27 +81,37 @@ class WifiViewModel: ObservableObject {
// networkModel.fetch()
}
@Published var pyleapProjects = [ResultItem]()
@Published var pyleapProjects = [PyProject]()
var boardDataProvider = BoardDataProvider()
// This function reads the boards the boot_out.txt file to then set the current board's name and version number for file transfer.
func setBoardToDefault() {
Board.shared.name = "Unrecognized Board"
Board.shared.versionNumber = "8"
}
func read() {
setBoardToDefault()
// This method can't be used until the device has permission to communicate.
print("READING CP Vers.")
wifiTransferService.getRequest(read: "boot_out.txt") { result in
let boardID = self.boardDataProvider.getBoardID(from: result) ?? "Unrecognized Board"
// Board default version is set to 8
let boardVersion = self.boardDataProvider.getCircuitPythonMajorVersion(from: result) ?? "8"
if result.contains("CircuitPython 7") {
WifiCPVersion.versionNumber = 7
print("WifiCPVersion.versionNumber set to: \(WifiCPVersion.versionNumber)")
}
if result.contains("CircuitPython 8") {
WifiCPVersion.versionNumber = 8
print("WifiCPVersion.versionNumber set to: \(WifiCPVersion.versionNumber)")
}
Board.shared.name = boardID
Board.shared.versionNumber = boardVersion
dump(Board.shared)
}
}
private weak var invalidIPObserver: NSObjectProtocol?
@ -146,12 +166,7 @@ class WifiViewModel: ObservableObject {
connectionStatus = .connected
}
// @Published var connectionStatus: ConnectionStatus = AppEnvironment.isRunningTests ? .connected : .noConnection
func printStoredInfo() {
print("======Stored UserDefaults======")

View file

@ -0,0 +1,53 @@
//
// BoardDataProvider.swift
// PyLeap
//
// Created by Trevor Beaton on 5/17/23.
//
import Foundation
final class BoardDataProvider {
func getBoardID(from prompt: String) -> String? {
print("getBoardID function: \(prompt)")
let boardIDPrefix = "Board ID:"
let uidSuffix = "UID"
if let startRange = prompt.range(of: boardIDPrefix) {
let start = startRange.upperBound
var end = prompt.endIndex
if let endRange = prompt.range(of: uidSuffix) {
end = endRange.lowerBound
}
let range = start..<end
let boardID = prompt[range].trimmingCharacters(in: .whitespacesAndNewlines)
let boardIDWithSpaces = boardID.replacingOccurrences(of: "_", with: " ")
let capitalizedBoardID = boardIDWithSpaces.capitalized
print("Outgoing: \(capitalizedBoardID)")
return capitalizedBoardID
}
return nil
}
func getCircuitPythonMajorVersion(from prompt: String) -> String? {
print("From getCircuitPythonMajorVersion function: \(prompt)")
let regexPattern = "CircuitPython (\\d+)"
if let regex = try? NSRegularExpression(pattern: regexPattern) {
let range = NSRange(location: 0, length: prompt.utf16.count)
if let match = regex.firstMatch(in: prompt, options: [], range: range) {
if let range = Range(match.range(at: 1), in: prompt) {
return String(prompt[range])
}
}
}
return nil
}
}

View file

@ -11,7 +11,7 @@ import Foundation
/// This is a DataStore class that is used for saving and loading data to/from the file system using the FileManager class.
*/
public class DataStore {
public class DataStore: ObservableObject {
let fileManager = FileManager.default
@ -27,8 +27,8 @@ public class DataStore {
/// This method writes it to a file named "StandardPyLeapProjects.json" in the documents directory.
*/
func save(content: [ResultItem], completion: @escaping () -> Void) {
func save(content: [PyProject], completion: @escaping () -> Void) {
print(#function)
let encoder = JSONEncoder()
if let encodedProjectData = try? encoder.encode(content) {
@ -47,7 +47,7 @@ public class DataStore {
/// This method reads the "StandardPyLeapProjects.json" file in the documents directory and decodes it as an array of ResultItem objects, and then appends the customProjects array to it and saves it back to the file.
*/
func save(customProjects: [ResultItem], completion: @escaping () -> Void) {
func save(customProjects: [PyProject], completion: @escaping () -> Void) {
var temp = customProjects
@ -55,7 +55,7 @@ public class DataStore {
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
let savedProjects = try? JSONDecoder().decode([PyProject].self, from: savedData) {
NotificationCenter.default.post(name: .didCollectCustomProject, object: nil, userInfo: nil)
temp.append(contentsOf: savedProjects)
@ -76,24 +76,24 @@ public class DataStore {
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
let savedProjects = try? JSONDecoder().decode([PyProject].self, from: savedData) {
loadCustomProjectList(contents: savedProjects)
}
}
/**
/// : This method reads the "StandardPyLeapProjects.json" file in the documents directory, decodes it as an array of ResultItem objects, and returns it.
/// This method reads the "StandardPyLeapProjects.json" file in the documents directory, decodes it as an array of ResultItem objects, and returns it.
*/
func loadDefaultList() -> [ResultItem] {
func loadDefaultList() -> [PyProject] {
var result = [ResultItem]()
var result = [PyProject]()
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
let savedProjects = try? JSONDecoder().decode([PyProject].self, from: savedData) {
result = savedProjects
}
return result
@ -107,14 +107,14 @@ public class DataStore {
/// This method reads the "CustomProjects.json" file in the documents directory, decodes it as an array of ResultItem objects, appends it to the input array, and then calls
*/
func loadCustomProjectList(contents: [ResultItem]) {
func loadCustomProjectList(contents: [PyProject]) {
var temp = contents
let fileURL = documentsDirectory.appendingPathComponent("CustomProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
let savedProjects = try? JSONDecoder().decode([PyProject].self, from: savedData) {
temp.append(contentsOf: savedProjects)
removeDuplicates(projectList: temp)
@ -129,9 +129,9 @@ public class DataStore {
/// This method uses the reduce(into:_:) method to iterate over the array, and it builds a new array that only contains unique ResultItem objects based on their bundleLink property. It then calls the save(content:completion:) method to save the new array to "StandardPyLeapProjects.json" file.
*/
func removeDuplicates(projectList: [ResultItem]) {
func removeDuplicates(projectList: [PyProject]) {
let combinedLists = projectList.reduce(into: [ResultItem]()) { (result, projectList) in
let combinedLists = projectList.reduce(into: [PyProject]()) { (result, projectList) in
if !result.contains(where: { $0.bundleLink == projectList.bundleLink }) {
result.append(projectList)
@ -146,7 +146,7 @@ public class DataStore {
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
let savedProjects = try? JSONDecoder().decode([PyProject].self, from: savedData) {
for project in savedProjects {
print("CustomProjects name: \(project.projectName)")

View file

@ -8,11 +8,24 @@
import Foundation
struct RootResults: Decodable {
let projects: [ResultItem]
struct RootResults: Codable {
let formatVersion: Int
let fileVersion: Int
let projects: [PyProject]
}
struct ResultItem: Codable, Identifiable, Equatable {
struct PyProject: Codable, Identifiable, Equatable {
var id: UUID = UUID()
let projectName: String
let projectImage: String
let description: String
let bundleLink: String
let learnGuideLink: String
let compatibility: [String]
let bluetoothCompatible: Bool
let wifiCompatible: Bool
enum CodingKeys: CodingKey {
case projectName
case projectImage
@ -20,15 +33,21 @@ struct ResultItem: Codable, Identifiable, Equatable {
case bundleLink
case learnGuideLink
case compatibility
case bluetoothCompatible
case wifiCompatible
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.projectName = try container.decode(String.self, forKey: .projectName)
self.projectImage = try container.decode(String.self, forKey: .projectImage)
self.description = try container.decode(String.self, forKey: .description)
self.bundleLink = try container.decode(String.self, forKey: .bundleLink)
self.learnGuideLink = try container.decode(String.self, forKey: .learnGuideLink)
self.compatibility = try container.decode([String].self, forKey: .compatibility)
self.bluetoothCompatible = try container.decode(Bool.self, forKey: .bluetoothCompatible)
self.wifiCompatible = try container.decode(Bool.self, forKey: .wifiCompatible)
}
var id = UUID()
let projectName: String
let projectImage: String
let description: String
let bundleLink: String
let learnGuideLink: String
let compatibility: [String]
}

View file

@ -14,6 +14,7 @@ import Foundation
import SwiftUI
class NetworkService: ObservableObject {
let dataStore = DataStore()
let thirdPartyBackgroundQueue = DispatchQueue(label: "com.PyLeap.thirdPartyBackgroundQueue", qos: .background, attributes: .concurrent)
@ -34,37 +35,38 @@ class NetworkService: ObservableObject {
func fetch(completion: @escaping() -> Void) {
print("Attempting Network Request")
let request = URLRequest(url: URL(string: AdafruitInfo.baseURL)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 60.0)
let request = URLRequest(url: URL(string: AdafruitInfo.baseURL)!)
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
print("error: \(error)")
}
if let data = data {
print("Updating UIList with new data...")
if let projectData = try? JSONDecoder().decode(RootResults.self, from: data) {
DispatchQueue.main.async {
self.dataStore.save(content: projectData.projects, completion: self.dataStore.loadDefaultProjectList)
completion()
}
} else {
print("No data found")
}
} else {
print("Updating UIList with Cached data...")
DispatchQueue.main.async {
self.dataStore.loadDefaultProjectList()
completion()
}
return
}
if let data = data {
print("Updating storage with new data.")
do {
let projectData = try JSONDecoder().decode(RootResults.self, from: data)
dump(projectData.projects)
DispatchQueue.main.async {
self.dataStore.save(content: projectData.projects, completion: self.dataStore.loadDefaultProjectList)
completion()
}
} catch {
print("Decoding error: \(error)")
}
}
}
task.resume()
}
func fetchThirdPartyProject(urlString: String?) {

View file

@ -175,6 +175,9 @@ Find more information on adding your own project here:
}
}
extension View {
func comfirmationAlertMessage(title: String, exitTitle: String, primaryTitle: String,disconnect: @escaping() -> (),cancel: @escaping() -> ()){

View file

@ -0,0 +1,55 @@
//
// KTBleManager.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
import Combine
/*
Wrapper to CBCentralManager
- adds separate publishers instead of a single delegate
- can be used to inject Test data
*/
protocol KTBleManager {
var bleState: CBManagerState { get }
var bleStatePublisher: PassthroughSubject<CBManagerState, Never> { get }
var bleDidDiscoverPublisher: PassthroughSubject<(CBPeripheral, [String: Any], Int), Never> { get }
var bleDidConnectPublisher: PassthroughSubject<CBPeripheral, Never> { get }
var bleDidFailToConnectPublisher: PassthroughSubject<(CBPeripheral, Error?), Never> { get }
var bleDidDisconnectPublisher: PassthroughSubject<(CBPeripheral, Error?), Never> { get }
func scanForPeripherals(withServices services: [CBUUID]?, options: [String : Bool]? )
func stopScan()
func connect(peripheral: CBPeripheral, options: [String : Bool]?)
func cancelPeripheralConnection(peripheral: CBPeripheral)
func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral]
func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheral]
}
// Warning: Notifications are deprecated and they will be removed. Use the combine publishers
// Notifications
enum NotificationUserInfoKey: String {
case uuid = "uuid"
case error = "error"
case state = "state"
}
// MARK: - Custom Notifications
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
public static let didUpdateBleState = Notification.Name(kPrefix+".didUpdateBleState")
public static let didDiscoverPeripheral = Notification.Name(kPrefix+".didDiscoverPeripheral")
public static let willConnectToPeripheral = Notification.Name(kPrefix+".willConnectToPeripheral")
public static let didConnectToPeripheral = Notification.Name(kPrefix+".didConnectToPeripheral")
public static let willDisconnectFromPeripheral = Notification.Name(kPrefix+".willDisconnectFromPeripheral")
public static let didDisconnectFromPeripheral = Notification.Name(kPrefix+".didDisconnectFromPeripheral")
}

View file

@ -0,0 +1,115 @@
//
// KTBleManagerImpl.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
import Combine
class KTBleManagerImpl: NSObject, KTBleManager {
// Ble
private var centralManager: CBCentralManager!
var bleState: CBManagerState {
return centralManager.state
}
// Publishers
let bleStatePublisher = PassthroughSubject<CBManagerState, Never>()
let bleDidDiscoverPublisher = PassthroughSubject<(CBPeripheral, [String: Any], Int), Never>()
let bleDidConnectPublisher = PassthroughSubject<CBPeripheral, Never>()
let bleDidFailToConnectPublisher = PassthroughSubject<(CBPeripheral, Error?), Never>()
let bleDidDisconnectPublisher = PassthroughSubject<(CBPeripheral, Error?), Never>()
// MARK: - Lifecycle
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.global(qos: .userInitiated), options: [:])
}
// MARK: - Actions
func scanForPeripherals(withServices services: [CBUUID]? = nil, options: [String : Bool]? = nil) {
centralManager.scanForPeripherals(withServices: services, options: options)
}
func stopScan() {
centralManager.stopScan()
}
func connect(peripheral: CBPeripheral, options: [String : Bool]?) {
centralManager.connect(peripheral, options: options)
}
func cancelPeripheralConnection(peripheral: CBPeripheral) {
centralManager.cancelPeripheralConnection(peripheral)
}
func retrievePeripherals(withIdentifiers identifiers: [UUID]) -> [CBPeripheral] {
return centralManager.retrievePeripherals(withIdentifiers: identifiers)
}
func retrieveConnectedPeripherals(withServices serviceUUIDs: [CBUUID]) -> [CBPeripheral] {
return centralManager.retrieveConnectedPeripherals(withServices: serviceUUIDs)
}
}
// MARK: - CBCentralManagerDelegate
extension KTBleManagerImpl: CBCentralManagerDelegate {
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
DLog("centralManagerDidUpdateState: \(central.state.rawValue)")
NotificationCenter.default.post(name: .didUpdateBleState, object: nil, userInfo: [NotificationUserInfoKey.state.rawValue: central.state])
bleStatePublisher.send(central.state)
}
/*
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
}*/
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
// DLog("didDiscover: \(peripheral.name ?? peripheral.identifier.uuidString)")
let rssi = RSSI.intValue
NotificationCenter.default.post(name: .didDiscoverPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
bleDidDiscoverPublisher.send((peripheral, advertisementData, rssi))
}
public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
DLog("didConnect: \(peripheral.name ?? peripheral.identifier.uuidString)")
// Send notification
NotificationCenter.default.post(name: .didConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
bleDidConnectPublisher.send(peripheral)
}
public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
DLog("didFailToConnect: \(peripheral.name ?? peripheral.identifier.uuidString). \(String(describing: error))")
bleDidFailToConnectPublisher.send((peripheral, error))
// Notify
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [
NotificationUserInfoKey.uuid.rawValue: peripheral.identifier,
NotificationUserInfoKey.error.rawValue: error as Any
])
}
public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
DLog("didDisconnectPeripheral: \(peripheral.name ?? peripheral.identifier.uuidString)")
bleDidDisconnectPublisher.send((peripheral, error))
// Notify
NotificationCenter.default.post(name: .didDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
}
}

View file

@ -0,0 +1,19 @@
//
// KTPeripheral.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
protocol KTPeripheral {
var name: String? { get }
var address: String { get }
var nameOrAddress: String { get }
var createdTime: CFAbsoluteTime { get }
func disconnect()
}

View file

@ -0,0 +1,122 @@
//
// KTBleAdvertisement+ManufacturerAdafruit.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
extension KTBleAdvertisement {
// Constants
internal static let kManufacturerAdafruitIdentifier: [UInt8] = [0x22, 0x08]
// MARK: - Check Manufacturer
public func isManufacturerAdafruit() -> Bool {
guard let manufacturerIdentifier = manufacturerIdentifier else { return false }
let manufacturerIdentifierBytes = [UInt8](manufacturerIdentifier)
//DLog("\(name) manufacturer: \(advertisement.manufacturerString)")
return manufacturerIdentifierBytes == Self.kManufacturerAdafruitIdentifier
}
/*
// MARK: - Adafruit Specific Data
struct AdafruitManufacturerData {
// Types
enum BoardModel: CaseIterable {
case circuitPlaygroundBluefruit
case clue_nRF52840
case feather_nRF52840_express
case feather_nRF52832
var identifier: [[UInt8]] { // Board identifiers used on the advertisement packet (USB PID)
switch self {
case .circuitPlaygroundBluefruit: return [[0x45, 0x80], [0x46, 0x80]]
case .clue_nRF52840: return [[0x71, 0x80], [0x72, 0x80]]
case .feather_nRF52840_express: return [[0x29, 0x80], [0x2A, 0x80]]
case .feather_nRF52832: return [[0x60, 0xEA]]
}
}
var neoPixelsCount: Int {
switch self {
case .circuitPlaygroundBluefruit: return 10
case .clue_nRF52840: return 1
case .feather_nRF52840_express: return 0
case .feather_nRF52832: return 0
}
}
}
// Data
var color: UIColor?
var boardModel: BoardModel?
// Utils
static func board(withBoardTypeData data: Data) -> BoardModel? {
let bytes = [UInt8](data)
let board = BoardModel.allCases.first(where: {
$0.identifier.contains(bytes)
})
return board
}
}
func adafruitManufacturerData() -> AdafruitManufacturerData? {
guard let manufacturerData = advertisement.manufacturerData else { return nil }
guard manufacturerData.count > 2 else { return nil } // It should have fields beyond the manufacturer identifier
var manufacturerFieldsData = Data(manufacturerData.dropFirst(2)) // Remove manufacturer identifier
var adafruitManufacturerData = AdafruitManufacturerData()
// Parse fields
let kHeaderLength = 1 + 2 // 1 byte len + 2 bytes key
while manufacturerFieldsData.count >= kHeaderLength {
// Parse current field
guard let fieldKey = Int16(data: manufacturerFieldsData[1...2]) else { return nil }
let fieldDataLenght = Int(manufacturerFieldsData[0]) - kHeaderLength // don't count header
let fieldData: Data
if manufacturerFieldsData.count >= kHeaderLength + fieldDataLenght {
fieldData = Data(manufacturerFieldsData[kHeaderLength...])
} else {
fieldData = Data()
}
// Decode field
switch fieldKey {
case 0 where fieldData.count >= 3: // Color
let r = fieldData[0]
let g = fieldData[1]
let b = fieldData[2]
adafruitManufacturerData.color = UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: 1)
case 1 where fieldData.count >= 2: // Board type
let boardTypeData = fieldData[0..<2]
if let board = AdafruitManufacturerData.board(withBoardTypeData: boardTypeData) {
adafruitManufacturerData.boardModel = board
}
else {
DLog("Warning: unknown board type found: \([UInt8](boardTypeData))")
}
default:
DLog("Error processing manufacturer data with key: \(fieldKey) len: \(fieldData.count) expectedLen: \(fieldDataLenght)")
break
}
// Remove processed field
manufacturerFieldsData = Data(manufacturerFieldsData.dropFirst(3 + fieldDataLenght))
}
return adafruitManufacturerData
}
*/
}

View file

@ -0,0 +1,64 @@
//
// KTBleAdvertisement.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
public struct KTBleAdvertisement {
var advertisementData: [String: Any]
init(advertisementData: [String: Any]?) {
self.advertisementData = advertisementData ?? [String: Any]()
}
// Advertisement data formatted
public var localName: String? {
return advertisementData[CBAdvertisementDataLocalNameKey] as? String
}
public var manufacturerData: Data? {
return advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
}
public var manufacturerHexDescription: String? {
guard let manufacturerData = manufacturerData else { return nil }
return KTHexUtils.hexDescription(data: manufacturerData)
// return String(data: manufacturerData, encoding: .utf8)
}
public var manufacturerIdentifier: Data? {
guard let manufacturerData = manufacturerData, manufacturerData.count >= 2 else { return nil }
let manufacturerIdentifierData = manufacturerData[0..<2]
return manufacturerIdentifierData
}
public var services: [CBUUID]? {
return advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
}
public var servicesOverflow: [CBUUID]? {
return advertisementData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID]
}
public var servicesSolicited: [CBUUID]? {
return advertisementData[CBAdvertisementDataSolicitedServiceUUIDsKey] as? [CBUUID]
}
public var serviceData: [CBUUID: Data]? {
return advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data]
}
public var txPower: Int? {
let number = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber
return number?.intValue
}
public var isConnectable: Bool? {
let connectableNumber = advertisementData[CBAdvertisementDataIsConnectable] as? NSNumber
return connectableNumber?.boolValue
}
}

View file

@ -0,0 +1,735 @@
//
// KTBlePeripheral.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
import Combine
class KTBlePeripheral: NSObject, KTPeripheral {
// Constants
static var kUndefinedRssiValue = 127
// Notifications
public enum NotificationUserInfoKey: String {
case uuid = "uuid"
case name = "name"
case invalidatedServices = "invalidatedServices"
}
enum PeripheralError: Error {
case timeout
case disconnection
case invalidState
}
// Data
var peripheral: CBPeripheral
var bleManager: KTBleManager
var identifier: UUID {
return peripheral.identifier
}
var name: String? {
return peripheral.name
}
var address: String {
return identifier.uuidString
}
var nameOrAddress: String { return name ?? address }
let createdTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
var lastSeenTime: CFAbsoluteTime
/*
var currentState: CBPeripheralState {
return peripheral.state
}*/
var state: CurrentValueSubject<CBPeripheralState, Never>
func maximumWriteValueLength(for: CBCharacteristicWriteType) -> Int {
return peripheral.maximumWriteValueLength(for: .withoutResponse)
}
var rssi: Int?
public var advertisement: KTBleAdvertisement
typealias CapturedReadCompletionHandler = ((_ value: Any?, _ error: Error?) -> Void)
private class CaptureReadHandler {
var identifier: String
var result: CapturedReadCompletionHandler
var timeoutTimer: Timer?
var timeoutAction: ((String) -> Void)?
var isNotifyOmitted: Bool
init(identifier: String, result: @escaping CapturedReadCompletionHandler, timeout: Double?, timeoutAction: ((String) -> Void)?, isNotifyOmitted: Bool = false) {
self.identifier = identifier
self.result = result
self.isNotifyOmitted = isNotifyOmitted
if let timeout = timeout {
self.timeoutAction = timeoutAction
DispatchQueue.global(qos: .background).async {
self.timeoutTimer = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(self.timerFired), userInfo: nil, repeats: false)
}
}
}
@objc private func timerFired() {
timeoutTimer?.invalidate()
timeoutTimer = nil
result(nil, PeripheralError.timeout)
timeoutAction?(identifier)
}
}
private func timeOutRemoveCaptureHandler(identifier: String) { // Default behaviour for a capture handler timeout
guard captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) else { return }
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
// Remove capture handler
captureReadHandlers.remove(at: index)
finishedExecutingCommand(error: PeripheralError.timeout)
}
// Internal data
private var connectionTimeoutTimer: Timer?
private var connectionCompletion: ((Result<Void, Error>) -> Void)?
private var stateObserverCancellable: Cancellable?
private var notifyHandlers = [String: ((Error?) -> Void)]() // Nofify handlers for each service-characteristic
private var captureReadHandlers = [CaptureReadHandler]()
private var commandQueue: KTCommandQueue<BleCommand>?
// MARK: - Init
public init(peripheral: CBPeripheral, bleManager: KTBleManager, advertisementData: [String: Any]?, rssi: Int?) {
self.peripheral = peripheral
self.bleManager = bleManager
self.advertisement = KTBleAdvertisement(advertisementData: advertisementData)
self.lastSeenTime = CFAbsoluteTimeGetCurrent()
self.state = CurrentValueSubject<CBPeripheralState, Never>(peripheral.state)
super.init()
self.rssi = rssi
/*
if AppEnvironment.isDebug, let name = peripheral.name, name.starts(with: "CIRCUIT") {
DLog("create peripheral: \(peripheral.name ?? peripheral.identifier.uuidString)")
}*/
// State observer
}
deinit {
DLog("blePeripheral deinit")
}
private func stateUpdatesEnabled(_ enabled: Bool) {
if enabled {
commandQueue = KTCommandQueue<BleCommand>(executeHandler: executeCommand)
stateObserverCancellable = peripheral.publisher(for: \.state)
//.removeDuplicates()
.receive(on: RunLoop.main)
.sink { [weak self] newState in
guard let self = self else { return }
guard newState != self.state.value else { return }
DLog("blePeripheral: \(self.nameOrAddress) status: \(newState.rawValue)")
switch newState {
case .connected:
let completionHandler = self.connectionCompletion
self.reset()
completionHandler?(.success(()))
case .disconnected:
let completionHandler = self.connectionCompletion
self.reset()
completionHandler?(.failure(PeripheralError.disconnection))
if self.peripheral.delegate === self {
self.peripheral.delegate = nil
}
self.stateObserverCancellable?.cancel()
self.stateObserverCancellable = nil
default:
break
}
self.state.value = newState
}
}
else {
stateObserverCancellable?.cancel()
stateObserverCancellable = nil
commandQueue?.removeAll()
commandQueue = nil
}
}
func reset() {
DLog("Command queue reset")
self.connectionCompletion = nil
connectionTimeoutTimer?.invalidate()
connectionTimeoutTimer = nil
rssi = nil
notifyHandlers.removeAll()
captureReadHandlers.removeAll()
commandQueue?.first()?.isCancelled = true // Stop current command if is processing
commandQueue?.removeAll()
}
// MARK: - Discover
func discover(serviceUuids: [CBUUID]?, completion: ((Error?) -> Void)?) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .discoverService, parameters: serviceUuids, completion: completion)
commandQueue.append(command)
}
func discover(characteristicUuids: [CBUUID]?, service: CBService, completion: ((Error?) -> Void)?) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .discoverCharacteristic, parameters: [characteristicUuids as Any, service], completion: completion)
commandQueue.append(command)
}
func discover(characteristicUuids: [CBUUID]?, serviceUuid: CBUUID, completion: ((Error?) -> Void)?) {
// Discover service
discover(serviceUuids: [serviceUuid]) { [unowned self] error in
guard error == nil else {
completion?(error)
return
}
guard let service = self.peripheral.services?.first(where: {$0.uuid == serviceUuid}) else {
completion?(BleCommand.CommandError.invalidService)
return
}
// Discover characteristic
self.discover(characteristicUuids: characteristicUuids, service: service, completion: completion)
}
}
func discoverDescriptors(characteristic: CBCharacteristic, completion: ((Error?) -> Void)?) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .discoverDescriptor, parameters: [characteristic], completion: completion)
commandQueue.append(command)
}
// MARK: - Connection
func connect(connectionTimeout: TimeInterval?, shouldNotifyOnConnection: Bool = false, shouldNotifyOnDisconnection: Bool = false, shouldNotifyOnNotification: Bool = false, completion: @escaping (Result<Void, Error>) -> Void) {
stateUpdatesEnabled(true)
self.connectionCompletion = completion
// Connect
NotificationCenter.default.post(name: .willConnectToPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: identifier])
var options: [String: Bool]?
if shouldNotifyOnConnection || shouldNotifyOnDisconnection || shouldNotifyOnNotification {
options = [CBConnectPeripheralOptionNotifyOnConnectionKey: shouldNotifyOnConnection, CBConnectPeripheralOptionNotifyOnDisconnectionKey: shouldNotifyOnDisconnection, CBConnectPeripheralOptionNotifyOnNotificationKey: shouldNotifyOnNotification]
}
if let timeout = connectionTimeout {
self.connectionTimeoutTimer = Timer.scheduledTimer(timeInterval: timeout, target: self, selector: #selector(self.connectionTimeoutFired), userInfo: identifier, repeats: false)
}
bleManager.connect(peripheral: peripheral, options: options)
// Wait for result via state changes
}
@objc private func connectionTimeoutFired(timer: Timer) {
self.connectionTimeoutTimer = nil
NotificationCenter.default.post(name: .willDisconnectFromPeripheral, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: identifier])
bleManager.cancelPeripheralConnection(peripheral: peripheral)
}
func disconnect() {
guard let commandQueue = commandQueue else { return }
let command = BleCommand(type: .disconnect, parameters: [bleManager], completion: nil)
commandQueue.append(command)
}
// MARK: - Service
func discoveredService(uuid: CBUUID) -> CBService? {
let service = peripheral.services?.first(where: {$0.uuid == uuid})
return service
}
func service(uuid: CBUUID, completion: ((CBService?, Error?) -> Void)?) {
if let discoveredService = discoveredService(uuid: uuid) { // Service was already discovered
completion?(discoveredService, nil)
} else {
discover(serviceUuids: [uuid], completion: { [unowned self] error in // Discover service
var discoveredService: CBService?
if error == nil {
discoveredService = self.discoveredService(uuid: uuid)
}
completion?(discoveredService, error)
})
}
}
// MARK: - Characteristic
func discoveredCharacteristic(uuid: CBUUID, service: CBService) -> CBCharacteristic? {
let characteristic = service.characteristics?.first(where: {$0.uuid == uuid})
return characteristic
}
func characteristic(uuid: CBUUID, service: CBService, completion: ((CBCharacteristic?, Error?) -> Void)?) {
if let discoveredCharacteristic = discoveredCharacteristic(uuid: uuid, service: service) { // Characteristic was already discovered
completion?(discoveredCharacteristic, nil)
} else {
discover(characteristicUuids: [uuid], service: service, completion: { [unowned self] (error) in // Discover characteristic
var discoveredCharacteristic: CBCharacteristic?
if error == nil {
discoveredCharacteristic = self.discoveredCharacteristic(uuid: uuid, service: service)
}
completion?(discoveredCharacteristic, error)
})
}
}
func characteristic(uuid: CBUUID, serviceUuid: CBUUID, completion: ((CBCharacteristic?, Error?) -> Void)?) {
if let discoveredService = discoveredService(uuid: serviceUuid) { // Service was already discovered
characteristic(uuid: uuid, service: discoveredService, completion: completion)
} else { // Discover service
service(uuid: serviceUuid) { (service, error) in
if let service = service, error == nil { // Discover characteristic
self.characteristic(uuid: uuid, service: service, completion: completion)
} else {
completion?(nil, error != nil ? error: BleCommand.CommandError.invalidService)
}
}
}
}
func enableNotify(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)?, completion: ((Error?) -> Void)? = nil) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .setNotify, parameters: [characteristic, true, handler as Any], completion: completion)
commandQueue.append(command)
}
func disableNotify(for characteristic: CBCharacteristic, completion: ((Error?) -> Void)? = nil) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .setNotify, parameters: [characteristic, false], completion: completion)
commandQueue.append(command)
}
func updateNotifyHandler(for characteristic: CBCharacteristic, handler: ((Error?) -> Void)? = nil) {
let identifier = handlerIdentifier(from: characteristic)
if notifyHandlers[identifier] == nil {
DLog("Warning: trying to update non-existent notifyHandler")
}
notifyHandlers[identifier] = handler
}
func readCharacteristic(_ characteristic: CBCharacteristic, completion readCompletion: @escaping CapturedReadCompletionHandler) {
guard let commandQueue = commandQueue else { readCompletion(nil, PeripheralError.invalidState); return }
let command = BleCommand(type: .readCharacteristic, parameters: [characteristic, readCompletion as Any], completion: nil)
commandQueue.append(command)
}
func write(data: Data, for characteristic: CBCharacteristic, type: CBCharacteristicWriteType, completion: ((Error?) -> Void)? = nil) {
guard let commandQueue = commandQueue else { completion?(PeripheralError.invalidState); return }
let command = BleCommand(type: .writeCharacteristic, parameters: [characteristic, type, data], completion: completion)
commandQueue.append(command)
}
func writeAndCaptureNotify(data: Data, for characteristic: CBCharacteristic, writeCompletion: ((Error?) -> Void)? = nil, readCharacteristic: CBCharacteristic, readTimeout: Double? = nil, readCompletion: CapturedReadCompletionHandler? = nil) {
guard let commandQueue = commandQueue else { writeCompletion?(PeripheralError.invalidState); return }
let type: CBCharacteristicWriteType = .withResponse // Force write with response
let command = BleCommand(type: .writeCharacteristicAndWaitNofity, parameters: [characteristic, type, data, readCharacteristic, readCompletion as Any, readTimeout as Any], timeout: readTimeout, completion: writeCompletion)
commandQueue.append(command)
}
// MARK: - Descriptors
func readDescriptor(_ descriptor: CBDescriptor, completion readCompletion: @escaping CapturedReadCompletionHandler) {
guard let commandQueue = commandQueue else { readCompletion(nil, PeripheralError.invalidState); return }
let command = BleCommand(type: .readDescriptor, parameters: [descriptor, readCompletion as Any], completion: nil)
commandQueue.append(command)
}
// MARK: - Rssi
func readRssi() {
peripheral.readRSSI()
}
// MARK: - Command Queue
internal class BleCommand: Equatable {
enum CommandType {
case discoverService
case discoverCharacteristic
case discoverDescriptor
case setNotify
case readCharacteristic
case writeCharacteristic
case writeCharacteristicAndWaitNofity
case readDescriptor
case disconnect
}
enum CommandError: Error {
case invalidService
}
var type: CommandType
var parameters: [Any]?
var completion: ((Error?) -> Void)?
var isCancelled = false
init(type: CommandType, parameters: [Any]?, timeout: Double? = nil, completion: ((Error?) -> Void)?) {
self.type = type
self.parameters = parameters
self.completion = completion
}
func completion(withError error: Error?) {
completion?(error)
}
static func == (left: BleCommand, right: BleCommand) -> Bool {
return left.type == right.type
}
}
private func executeCommand(command: BleCommand) {
// Force update peripheral delegate in case other BlePeripherals have been created with the same CBPeripheral
self.peripheral.delegate = self
switch command.type {
case .discoverService:
discoverService(with: command)
case .discoverCharacteristic:
discoverCharacteristic(with: command)
case .discoverDescriptor:
discoverDescriptor(with: command)
case .setNotify:
setNotify(with: command)
case .readCharacteristic:
readCharacteristic(with: command)
case .writeCharacteristic, .writeCharacteristicAndWaitNofity:
write(with: command)
case .readDescriptor:
readDescriptor(with: command)
case .disconnect:
disconnect(with: command)
}
}
private func handlerIdentifier(from characteristic: CBCharacteristic) -> String {
guard let service = characteristic.service else { DLog("Error: handleIdentifier with nil characteritic service"); return "" }
return "\(service.uuid.uuidString)-\(characteristic.uuid.uuidString)"
}
private func handlerIdentifier(from descriptor: CBDescriptor) -> String {
guard let characteristic = descriptor.characteristic, let service = characteristic.service else { DLog("Error: handleIdentifier with nil descriptor service"); return "" }
return "\(service.uuid.uuidString)-\(characteristic.uuid.uuidString)-\(descriptor.uuid.uuidString)"
}
internal func finishedExecutingCommand(error: Error?) {
//DLog("finishedExecutingCommand")
guard let commandQueue = commandQueue else { return }
// Result Callback
if let command = commandQueue.first(), !command.isCancelled {
command.completion(withError: error)
}
commandQueue.executeNext()
}
// MARK: - Commands
private func discoverService(with command: BleCommand) {
var serviceUuids = command.parameters as? [CBUUID]
let discoverAll = serviceUuids == nil
// Remove services already discovered from the query
if let services = peripheral.services, let serviceUuidsToDiscover = serviceUuids {
for (i, serviceUuid) in serviceUuidsToDiscover.enumerated().reversed() {
if services.contains(where: {$0.uuid == serviceUuid}) {
serviceUuids!.remove(at: i)
}
}
}
// Discover remaining uuids
if discoverAll || (serviceUuids != nil && serviceUuids!.count > 0) {
peripheral.discoverServices(serviceUuids)
} else {
// Everything was already discovered
finishedExecutingCommand(error: nil)
}
}
private func discoverCharacteristic(with command: BleCommand) {
var characteristicUuids = command.parameters![0] as? [CBUUID]
let discoverAll = characteristicUuids == nil
let service = command.parameters![1] as! CBService
// Remove services already discovered from the query
if let characteristics = service.characteristics, let characteristicUuidsToDiscover = characteristicUuids {
for (i, characteristicUuid) in characteristicUuidsToDiscover.enumerated().reversed() {
if characteristics.contains(where: {$0.uuid == characteristicUuid}) {
characteristicUuids!.remove(at: i)
}
}
}
// Discover remaining uuids
if discoverAll || (characteristicUuids != nil && characteristicUuids!.count > 0) {
//DLog("discover \(characteristicUuids == nil ? "all": String(characteristicUuids!.count)) characteristics for \(service.uuid.uuidString)")
peripheral.discoverCharacteristics(characteristicUuids, for: service)
} else {
// Everthing was already discovered
finishedExecutingCommand(error: nil)
}
}
private func discoverDescriptor(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
peripheral.discoverDescriptors(for: characteristic)
}
private func setNotify(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
let enabled = command.parameters![1] as! Bool
let identifier = handlerIdentifier(from: characteristic)
if enabled {
let handler = command.parameters![2] as? ((Error?) -> Void)
notifyHandlers[identifier] = handler
} else {
notifyHandlers.removeValue(forKey: identifier)
}
peripheral.setNotifyValue(enabled, for: characteristic)
}
private func readCharacteristic(with command: BleCommand) {
let characteristic = command.parameters!.first as! CBCharacteristic
let completion = command.parameters![1] as! CapturedReadCompletionHandler
let identifier = handlerIdentifier(from: characteristic)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
peripheral.readValue(for: characteristic)
}
private func write(with command: BleCommand) {
let characteristic = command.parameters![0] as! CBCharacteristic
let writeType = command.parameters![1] as! CBCharacteristicWriteType
let data = command.parameters![2] as! Data
if writeType == .withoutResponse {
let mtu = maximumWriteValueLength(for: .withoutResponse)
var offset = 0
while offset < data.count {
let chunkData = data.subdata(in: offset ..< min(offset + mtu, data.count))
//DLog("blewrite offset: \(offset) / \(data.count), size: \(chunkData.count)")
peripheral.writeValue(chunkData, for: characteristic, type: .withoutResponse)
offset += chunkData.count
}
if !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
let readCharacteristic = command.parameters![3] as! CBCharacteristic
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
let timeout = command.parameters![5] as? Double
let identifier = handlerIdentifier(from: readCharacteristic)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
}
finishedExecutingCommand(error: nil)
}
else {
peripheral.writeValue(data, for: characteristic, type: writeType)
}
}
private func readDescriptor(with command: BleCommand) {
let descriptor = command.parameters!.first as! CBDescriptor
let completion = command.parameters![1] as! CapturedReadCompletionHandler
let identifier = handlerIdentifier(from: descriptor)
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: completion, timeout: nil, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
peripheral.readValue(for: descriptor)
}
internal func disconnect(with command: BleCommand) {
let bleManager = command.parameters!.first as! KTBleManager
bleManager.cancelPeripheralConnection(peripheral: self.peripheral)
finishedExecutingCommand(error: nil)
}
}
// MARK: - CBPeripheralDelegate
extension KTBlePeripheral: CBPeripheralDelegate {
public func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
DLog("peripheralDidUpdateName: \(name ?? "{ No Name }")")
NotificationCenter.default.post(name: .peripheralDidUpdateName, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.name.rawValue: name as Any])
}
public func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
DLog("didModifyServices")
NotificationCenter.default.post(name: .peripheralDidModifyServices, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier, NotificationUserInfoKey.invalidatedServices.rawValue: invalidatedServices])
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
//DLog("didDiscoverServices for: \(peripheral.name ?? peripheral.identifier.uuidString)")
finishedExecutingCommand(error: error)
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
///DLog("didDiscoverCharacteristicsFor: \(service.uuid.uuidString)")
finishedExecutingCommand(error: error)
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
finishedExecutingCommand(error: error)
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
let identifier = handlerIdentifier(from: characteristic)
/*
if (BlePeripheral.kProfileCharacteristicUpdates) {
let currentTime = CACurrentMediaTime()
let elapsedTime = currentTime - profileStartTime
DLog("elapsed: \(String(format: "%.1f", elapsedTime * 1000))")
profileStartTime = currentTime
}
*/
//DLog("didUpdateValueFor \(characteristic.uuid.uuidString): \(String(data: characteristic.value ?? Data(), encoding: .utf8) ?? "<invalid>")")
// Check if waiting to capture this read
var isNotifyOmitted = false
var hasCaptureHandler = false
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
hasCaptureHandler = true
// DLog("captureReadHandlers index: \(index) / \(captureReadHandlers.count)")
// Remove capture handler
let captureReadHandler = captureReadHandlers.remove(at: index)
// DLog("captureReadHandlers postRemove count: \(captureReadHandlers.count)")
// Cancel timeout timer
captureReadHandler.timeoutTimer?.invalidate()
captureReadHandler.timeoutTimer = nil
// Send result
let value = characteristic.value
// DLog("updated value: \(String(data: value!, encoding: .utf8)!)")
captureReadHandler.result(value, error)
isNotifyOmitted = captureReadHandler.isNotifyOmitted
}
// Notify
if !isNotifyOmitted {
if let notifyHandler = notifyHandlers[identifier] {
//let currentTime = CACurrentMediaTime()
notifyHandler(error)
//DLog("elapsed: \(String(format: "%.1f", (CACurrentMediaTime() - currentTime) * 1000))")
}
}
if hasCaptureHandler {
finishedExecutingCommand(error: error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
guard let commandQueue = commandQueue else { return }
if let command = commandQueue.first(), !command.isCancelled, command.type == .writeCharacteristicAndWaitNofity {
let characteristic = command.parameters![3] as! CBCharacteristic
let readCompletion = command.parameters![4] as! CapturedReadCompletionHandler
let timeout = command.parameters![5] as? Double
let identifier = handlerIdentifier(from: characteristic)
//DLog("read timeout started")
let captureReadHandler = CaptureReadHandler(identifier: identifier, result: readCompletion, timeout: timeout, timeoutAction: timeOutRemoveCaptureHandler)
captureReadHandlers.append(captureReadHandler)
} else {
finishedExecutingCommand(error: error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
finishedExecutingCommand(error: error)
}
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
let identifier = handlerIdentifier(from: descriptor)
if captureReadHandlers.count > 0, let index = captureReadHandlers.firstIndex(where: {$0.identifier == identifier}) {
// Remove capture handler
let captureReadHandler = captureReadHandlers.remove(at: index)
// Send result
let value = descriptor.value
captureReadHandler.result(value, error)
finishedExecutingCommand(error: error)
}
}
public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
guard error == nil else { DLog("didReadRSSI error: \(error!.localizedDescription)"); return }
let rssi = RSSI.intValue
if rssi != KTBlePeripheral.kUndefinedRssiValue { // only update rssi value if is defined ( 127 means undefined )
self.rssi = rssi
}
NotificationCenter.default.post(name: .peripheralDidUpdateRssi, object: nil, userInfo: [NotificationUserInfoKey.uuid.rawValue: peripheral.identifier])
}
}
// MARK: - Custom Notifications
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
public static let peripheralDidUpdateName = Notification.Name(kPrefix+".peripheralDidUpdateName")
public static let peripheralDidModifyServices = Notification.Name(kPrefix+".peripheralDidModifyServices")
public static let peripheralDidUpdateRssi = Notification.Name(kPrefix+".peripheralDidUpdateRssi")
}

View file

@ -0,0 +1,87 @@
//
// KTSavedBondedBlePeripherals.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
class KTSavedBondedBlePeripherals: ObservableObject {
// Constants
private static let dataKey = "data"
// Parameters
var userDefaults: UserDefaults // Can be replaced if data saved needs to be shared
struct PeripheralData: Codable {
var name: String?
let uuid: UUID
}
// Published
@Published var peripheralsData = [PeripheralData]()
// Internal data
private var peripheralsDataLock = NSLock()
init(userDefaults: UserDefaults = UserDefaults.standard) {
self.userDefaults = userDefaults
self.peripheralsData = getPeripheralsData()
}
// MARK: - Actions
func add(name: String?, uuid: UUID) {
peripheralsDataLock.lock(); defer { peripheralsDataLock.unlock() }
// If already exist that address, remove it and add it to update the name
let existingPeripheral = peripheralsData.first { $0.uuid == uuid }
// Continue if not exist or the name has changed
if existingPeripheral == nil || existingPeripheral!.name != name {
// If the name has changed, remove it to add it with the new name
if existingPeripheral != nil {
peripheralsData.removeAll { $0.uuid == uuid }
}
peripheralsData.append(PeripheralData(name: name, uuid: uuid))
setBondedPeripherals(peripheralsData)
}
}
func remove(uuid: UUID) {
peripheralsDataLock.lock(); defer { peripheralsDataLock.unlock() }
peripheralsData.removeAll { $0.uuid == uuid }
setBondedPeripherals(peripheralsData)
}
func clear() {
peripheralsDataLock.lock(); defer { peripheralsDataLock.unlock() }
setBondedPeripherals([])
}
private func getPeripheralsData() -> [PeripheralData] {
guard let data = userDefaults.object(forKey: Self.dataKey) as? Data,
let result = try? JSONDecoder().decode([PeripheralData].self, from: data) else {
return []
}
return result
}
// MARK: - Utils
private func setBondedPeripherals(_ peripherals: [PeripheralData]) {
if let encoded = try? JSONEncoder().encode(peripheralsData) {
userDefaults.set(encoded, forKey: Self.dataKey)
}
peripheralsData = peripherals
}
}

View file

@ -0,0 +1,21 @@
//
// KTBlePeripheralScanner.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
protocol KTBlePeripheralScanner: ObservableObject {
var blePeripherals: [KTBlePeripheral] { get }
var blePeripheralsPublisher: Published<[KTBlePeripheral]>.Publisher { get }
//var bleLastError: Error? { get }
var bleLastErrorPublisher: Published<Error?>.Publisher { get }
func start()
func stop()
func clearBleLastException()
}

View file

@ -0,0 +1,24 @@
//
// KTBlePeripheralScannerFake.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
class BlePeripheralScannerFake: KTBlePeripheralScanner {
@Published private(set) var blePeripherals = [KTBlePeripheral]()
var blePeripheralsPublisher: Published<[KTBlePeripheral]>.Publisher { $blePeripherals }
@Published private(set) var bleLastError: Error? = nil
var bleLastErrorPublisher: Published<Error?>.Publisher { $bleLastError }
func start() {}
func stop() {}
func clearBleLastException() {
bleLastError = nil
}
}

View file

@ -0,0 +1,213 @@
//
// KTBlePeripheralScannerImpl.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
import Foundation
import CoreBluetooth
import QuartzCore
import Combine
class BlePeripheralScannerImpl: KTBlePeripheralScanner {
// Configuration
private static let kStopScanningWhenConnectingToPeripheral = false
private static let kAlwaysAllowDuplicateKeys = true
// BlePeripheralScanner
@Published private(set) var blePeripherals = [KTBlePeripheral]()
var blePeripheralsPublisher: Published<[KTBlePeripheral]>.Publisher { $blePeripherals }
@Published private(set) var bleLastError: Error? = nil
var bleLastErrorPublisher: Published<Error?>.Publisher { $bleLastError }
// Params
var scanningServicesFilter: [CBUUID]? = nil
// Scanning
public var isScanning: Bool {
return scanningStartTime != nil
}
public var scanningElapsedTime: TimeInterval? {
guard let scanningStartTime = scanningStartTime else { return nil }
return CACurrentMediaTime() - scanningStartTime
}
private var isScanningWaitingToStart = false
internal var scanningStartTime: TimeInterval? // Time when the scanning started. nil if stopped
internal var peripheralsFound = [UUID: KTBlePeripheral]()
internal var peripheralsFoundLock = NSLock()
private let bleManager: KTBleManager
private let includeConnectedPeripheralsWithServiceId: CBUUID?
private var disposables = Set<AnyCancellable>()
// MARK: - Lifecycle
init(bleManager: KTBleManager, includeConnectedPeripheralsWithServiceId: CBUUID?) {
self.bleManager = bleManager
self.includeConnectedPeripheralsWithServiceId = includeConnectedPeripheralsWithServiceId
// Ble status observer
bleManager.bleStatePublisher.sink { [weak self] state in
self?.onBleStateChanged(state: state)
}
.store(in: &disposables)
// Ble discover observer
bleManager.bleDidDiscoverPublisher.sink { [weak self] (peripheral, advertisementData, rssi) in
self?.onPeripheralDiscovered(peripheral: peripheral, advertisementData: advertisementData, rssi: rssi)
}
.store(in: &disposables)
// Ble connection failure observer
bleManager.bleDidFailToConnectPublisher.sink { [weak self] (peripheral, error) in
self?.onPeripheralDidFailToConnect(peripheral: peripheral, error: error)
}
.store(in: &disposables)
// Ble disconnection observer
bleManager.bleDidDisconnectPublisher.sink { [weak self] (peripheral, error) in
self?.onPeripheralDidDisconnect(peripheral: peripheral, error: error)
}
.store(in: &disposables)
}
deinit {
scanningServicesFilter?.removeAll()
peripheralsFound.removeAll()
}
// MARK: - Scan
func start() {
stop()
isScanningWaitingToStart = true
let bleState = bleManager.bleState
guard bleState != .poweredOff && bleState != .unauthorized && bleState != .unsupported else {
DLog("startScan failed because central manager is not ready")
return
}
guard bleState == .poweredOn else {
DLog("startScan failed because central manager is not powered on")
return
}
if let includeConnectedPeripheralsWithServiceId = includeConnectedPeripheralsWithServiceId {
self.discoverConnectedPeripherals(serviceId: includeConnectedPeripheralsWithServiceId)
}
// DLog("start scan")
scanningStartTime = CACurrentMediaTime()
NotificationCenter.default.post(name: .didStartScanning, object: nil)
let options = Self.kAlwaysAllowDuplicateKeys ? [CBCentralManagerScanOptionAllowDuplicatesKey: true] : nil
bleManager.scanForPeripherals(withServices: scanningServicesFilter, options: options)
isScanningWaitingToStart = false
}
func stop() {
// DLog("stop scan")
bleManager.stopScan()
scanningStartTime = nil
isScanningWaitingToStart = false
NotificationCenter.default.post(name: .didStopScanning, object: nil)
}
func clearBleLastException() {
bleLastError = nil
}
private func onBleStateChanged(state: CBManagerState) {
if state == .poweredOn {
if self.isScanningWaitingToStart {
self.start() // Continue scanning now that bluetooth is back
}
}
else {
if self.isScanning {
self.isScanningWaitingToStart = true
}
self.scanningStartTime = nil
// Remove all peripherals found (Important because the BlePeripheral queues could contain old commands that were processing when the bluetooth state changed)
self.peripheralsFoundLock.lock(); defer { self.peripheralsFoundLock.unlock() }
self.peripheralsFound.values.forEach { blePeripheral in
blePeripheral.reset()
}
self.peripheralsFound.removeAll()
}
}
private func onPeripheralDiscovered(peripheral: CBPeripheral, advertisementData: [String: Any]? = nil, rssi: Int? = nil) {
peripheralsFoundLock.lock(); defer { peripheralsFoundLock.unlock() }
/*
if AppEnvironment.isDebug, peripheral.name?.starts(with: "CIRCUIT") != true {
return
}*/
if let existingPeripheral = peripheralsFound[peripheral.identifier] {
existingPeripheral.lastSeenTime = CFAbsoluteTimeGetCurrent()
if let rssi = rssi, rssi != KTBlePeripheral.kUndefinedRssiValue { // only update rssi value if is defined ( 127 means undefined )
existingPeripheral.rssi = rssi
}
if let advertisementData = advertisementData {
for (key, value) in advertisementData {
existingPeripheral.advertisement.advertisementData.updateValue(value, forKey: key)
}
}
peripheralsFound[peripheral.identifier] = existingPeripheral
} else { // New peripheral found
let blePeripheral = KTBlePeripheral(peripheral: peripheral, bleManager: bleManager, advertisementData: advertisementData, rssi: rssi)
peripheralsFound[peripheral.identifier] = blePeripheral
}
blePeripherals = Array(peripheralsFound.values)
}
private func onPeripheralDidFailToConnect(peripheral: CBPeripheral, error: Error?) {
peripheralsFound[peripheral.identifier]?.reset()
bleLastError = error
}
private func onPeripheralDidDisconnect(peripheral: CBPeripheral, error: Error?) {
// Remove from peripheral list (after sending notification so the receiving objects can query about the peripheral before being removed)
peripheralsFoundLock.lock()
peripheralsFound.removeValue(forKey: peripheral.identifier)
peripheralsFoundLock.unlock()
bleLastError = error
}
// MARK: - Connected Peripherals
func discoverConnectedPeripherals(serviceId: CBUUID) {
let peripheralsWithService = bleManager.retrieveConnectedPeripherals(withServices: [serviceId])
if !peripheralsWithService.isEmpty {
//let existingPeripheralsIdentifiers = Array(peripheralsFound.keys)
for peripheral in peripheralsWithService {
//if !existingPeripheralsIdentifiers.contains(peripheral.identifier) {
DLog("Connected peripheral with known service: \(peripheral.name ?? peripheral.identifier.uuidString)")
let advertisementData = [CBAdvertisementDataServiceUUIDsKey: [serviceId]]
self.onPeripheralDiscovered(peripheral: peripheral, advertisementData: advertisementData)
//}
}
}
}
}
// MARK: - Custom Notifications
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
public static let didStartScanning = Notification.Name(kPrefix+".didStartScanning")
public static let didStopScanning = Notification.Name(kPrefix+".didStopScanning")
public static let didUnDiscoverPeripheral = Notification.Name(kPrefix+".didUnDiscoverPeripheral")
}

View file

@ -0,0 +1,430 @@
//
// KTConnectionManager.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import Combine
import CoreBluetooth
class KTConnectionManager: ObservableObject {
// Constants
private static let kReconnectTimeout: TimeInterval = 5
// Published
@Published var scanningState: Scanner.ScanningState = .idle
@Published var peripherals = [KTPeripheral]()
@Published var currentFileTransferClient: KTFileTransferClient? = nil
@Published var isReconnectingToBondedPeripherals = false
@Published var peripheralAddressesBeingSetup = Set<String>()
var bleScanningLastErrorPublisher: Published<Error?>.Publisher
@Published var lastReconnectionError: Error?
// Data
let bleManager: KTBleManager
private let scanner: Scanner
private let onBlePeripheralBonded: ((_ name: String, _ uuid: UUID) -> Void)?
private var disposables = Set<AnyCancellable>()
private var fileTransferClients = [String: KTFileTransferClient]() // FileTransferClient for each peripheral
private var managedBlePeripherals = [(KTBleFileTransferPeripheral, Cancellable)]()
/// Is reconnecting the peripheral with identifier
private var isReconnectingPeripheral = [String: Bool]()
private var isDisconnectingPeripheral = [String: Bool]()
/// User selected client (or picked automatically by the system if user didn't pick or got disconnected)
private var userSelectedTransferClient: KTFileTransferClient? = nil
public enum ConnectionError: Error {
case unknownPeripheralAddress
case undefinedPeripheralType
case cannotConnectToBondedPeripheral
}
struct BondedPeripheralData {
var name: String?
let uuid: UUID
let state: CBPeripheralState?
}
// MARK: - Lifecycle
init(
bleManager: any KTBleManager,
blePeripheralScanner: any KTBlePeripheralScanner,
onBlePeripheralBonded: ((_ name: String, _ uuid: UUID) -> Void)?) {
self.bleManager = bleManager
self.scanner = Scanner(blePeripheralScanner: blePeripheralScanner)
self.onBlePeripheralBonded = onBlePeripheralBonded
bleScanningLastErrorPublisher = scanner.bleLastErrorPublisher
scanner.$scanningState
.throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true)
//.receive(on: RunLoop.main)
.sink { state in
// Update state
self.scanningState = state
// Update peripherals
if case let .scanning(peripherals) = state {
self.peripherals = peripherals
}
else {
self.peripherals = []
}
}
.store(in: &disposables)
}
// MARK: - Actions
func startScan() {
scanner.start()
}
func stopScan() {
scanner.stop()
}
func connect(knownAddress: String, completion: @escaping ((Result<KTFileTransferClient, Error>) -> Void)) {
guard let peripheral = peripherals.first (where: { $0.address == knownAddress }) else {
completion(.failure(ConnectionError.unknownPeripheralAddress))
return
}
connect(peripheral: peripheral, completion: completion)
}
private func connect(peripheral: KTPeripheral, completion: @escaping ((Result<KTFileTransferClient, Error>) -> Void)) {
var fileTransferPeripheral: KTFileTransferPeripheral?
switch peripheral {
case let blePeripheral as KTBlePeripheral:
fileTransferPeripheral = KTBleFileTransferPeripheral(
blePeripheral: blePeripheral,
onBonded: onBlePeripheralBonded
)
default:
fileTransferPeripheral = nil
}
guard let fileTransferPeripheral = fileTransferPeripheral else {
completion(.failure(ConnectionError.undefinedPeripheralType))
return
}
// Connect
connect(fileTransferPeripheral: fileTransferPeripheral, completion: completion)
}
func setSelectedPeripheral(peripheral: KTPeripheral) {
if let fileTransferClient = fileTransferClients[peripheral.address] {
userSelectedTransferClient = fileTransferClient
updateSelectedPeripheral()
}
else {
connect(peripheral: peripheral) { [weak self] result in
guard let self = self else { return }
let fileTransferClient = try? result.get()
self.userSelectedTransferClient = fileTransferClient
self.updateSelectedPeripheral()
}
}
}
func reconnectToBondedBlePeripherals(knownUuids identifiers: [UUID], timeout: TimeInterval? = nil, completion: ( (_ connectedBlePeripherals: [KTFileTransferClient]) -> Void)?) {
let knownPeripherals = bleManager.retrievePeripherals(withIdentifiers: identifiers)
guard !knownPeripherals.isEmpty else { DLog("knownPeripherals isEmpty"); return }
isReconnectingToBondedPeripherals = true
reconnectToBondedPeripherals(knownPeripherals: knownPeripherals, timeout: Self.kReconnectTimeout) { [weak self] readyBleFileTransferClients in
guard let self = self else { return }
self.isReconnectingToBondedPeripherals = false
if let firstConnectedBleFileTransferClient = readyBleFileTransferClients.first {
DLog("Reconnected to \(firstConnectedBleFileTransferClient.peripheral.nameOrAddress)")
self.setSelectedPeripheral(peripheral: firstConnectedBleFileTransferClient.peripheral)
}
else { // Is empty
DLog("Cannot connect to bonded peripheral")
self.lastReconnectionError = ConnectionError.cannotConnectToBondedPeripheral
}
completion?(readyBleFileTransferClients)
}
}
/*
func reconnectToAlreadyConnectedPeripherals(withServices services: [CBUUID], timeout: TimeInterval?, completion: @escaping (_ connectedBlePeripherals: [BleFileTransferPeripheral]) -> Void) {
var connectedAndSetupPeripherals = [BleFileTransferPeripheral]()
let peripheralsWithServices = bleManager.retrieveConnectedPeripherals(withServices: services)
guard !peripheralsWithServices.isEmpty else { completion(connectedAndSetupPeripherals); return }
// TODO
}*/
private func reconnectToBondedPeripherals(knownPeripherals: [CBPeripheral], timeout: TimeInterval? = nil, completion: @escaping (_ connectedBlePeripherals: [KTFileTransferClient]) -> Void) {
var connectedAndSetupPeripherals = [KTFileTransferClient]()
let knownUuids = knownPeripherals.map{$0.identifier}
var awaitingConnection = knownUuids
func connectionFinished(knownPeripheral: CBPeripheral) {
awaitingConnection.removeAll { $0 == knownPeripheral.identifier }
// Call completion when all awaiting peripherals have finished reconnection
if awaitingConnection.isEmpty {
completion(connectedAndSetupPeripherals)
}
}
for knownPeripheral in knownPeripherals {
DLog("Try to connect to known peripheral: \(knownPeripheral.identifier)")
if knownPeripheral.state == .connected, let fileTransferClient = fileTransferClient(address: knownPeripheral.identifier.uuidString) {
connectedAndSetupPeripherals.append(fileTransferClient)
connectionFinished(knownPeripheral: knownPeripheral)
setSelectedPeripheral(peripheral: fileTransferClient.peripheral)
}
else if knownPeripheral.state == .connected || knownPeripheral.state == .disconnected {
let blePeripheral = KTBlePeripheral(peripheral: knownPeripheral, bleManager: bleManager, advertisementData: nil, rssi: nil)
let bleFileTransferPeripheral = KTBleFileTransferPeripheral(
blePeripheral: blePeripheral,
onBonded: onBlePeripheralBonded
)
connect(fileTransferPeripheral: bleFileTransferPeripheral, connectionTimeout: timeout) { result in
switch result {
case .success(let fileTransferClient):
connectedAndSetupPeripherals.append(fileTransferClient)
case .failure:
break
}
connectionFinished(knownPeripheral: knownPeripheral)
}
}
else {
DLog("warning: trying to connect to a peripheral that is transient state: \(knownPeripheral.name ?? knownPeripheral.identifier.uuidString)")
connectionFinished(knownPeripheral: knownPeripheral)
}
}
}
func connect(
fileTransferPeripheral: KTFileTransferPeripheral,
connectionTimeout: TimeInterval? = nil,
completion: @escaping ((Result<KTFileTransferClient, Error>) -> Void)
) {
peripheralAddressesBeingSetup.insert(fileTransferPeripheral.peripheral.address)
fileTransferPeripheral.connectAndSetup(connectionTimeout: connectionTimeout) { [weak self] result in
guard let self = self else { return }
DLog("FileTransferClient connect success: \(result.isSuccess)")
DispatchQueue.main.async {
self.peripheralAddressesBeingSetup.remove(fileTransferPeripheral.peripheral.address)
switch result {
case .success:
let fileTransferClient = KTFileTransferClient(fileTransferPeripheral: fileTransferPeripheral)
self.fileTransferClients[fileTransferPeripheral.peripheral.address] = fileTransferClient
self.updateSelectedPeripheral()
// If is a Bluetooth Peripheral, add it to managed connections
if let bleFileTransferPeripheral = fileTransferPeripheral as? KTBleFileTransferPeripheral {
self.addPeripheralToAutomaticallyManagedBleConnection(fileTransferPeripheral: bleFileTransferPeripheral)
}
completion(.success(fileTransferClient))
case .failure(let error):
completion(.failure(error))
}
}
}
}
func disconnectFileTransferClient(address: String) {
// Remove from managed connections
// removePeripheralFromAutomaticallyManagedConnection(address: address)
self.isDisconnectingPeripheral[address] = true
// If is the user selected peripheral, set the new user selected to nil
if let existingFileTransferClient = fileTransferClient(address: address), userSelectedTransferClient?.peripheral.address == existingFileTransferClient.peripheral.address {
userSelectedTransferClient = nil
}
// Disconnect if exists
fileTransferClients[address]?.peripheral.disconnect()
// Update
updateSelectedPeripheral()
}
// MARK: - Get state
func fileTransferClient(address: String) -> KTFileTransferClient? {
return fileTransferClients[address]
}
/*
Receives an array of SavedBondedBlePeripherals.PeripheralData and returns the same array but adding internal peripheral info [BondedPeripheralData]
Useful for UI
*/
func bondedBlePeripheralDataWithState(peripheralsData: [KTSavedBondedBlePeripherals.PeripheralData]) -> [BondedPeripheralData] {
return peripheralsData.map {
let state = (fileTransferClient(address: $0.uuid.uuidString)?.peripheral as? KTBlePeripheral)?.state.value
return BondedPeripheralData(
name: $0.name,
uuid: $0.uuid,
state: state
)
}
}
// MARK: - Internal
private func updateSelectedPeripheral() {
// Update selectedFileTransferClient
let fileTransferClient = userSelectedTransferClient ?? fileTransferClients.values.first
if fileTransferClient?.peripheral.address != currentFileTransferClient?.peripheral.address {
currentFileTransferClient = fileTransferClient
DLog("selectedPeripheral: \(currentFileTransferClient?.peripheral.nameOrAddress ?? "-")")
}
}
// MARK: - Managed Peripherals
private func addPeripheralToAutomaticallyManagedBleConnection(fileTransferPeripheral: KTBleFileTransferPeripheral) {
// Check that doesn't already exists
guard !managedBlePeripherals.contains(where: { $0.0.address == fileTransferPeripheral.address }) else {
DLog("trying to add an already managed peripheral: \(fileTransferPeripheral.nameOrAddress)")
return
}
let cancellable = fileTransferPeripheral.fileTransferState
.sink(receiveCompletion: { completion in
DLog("File transfer state error. Force disconnect")
fileTransferPeripheral.peripheral.disconnect()
self.removePeripheralFromAutomaticallyManagedConnection(bleFileTransferPeripheral: fileTransferPeripheral)
}, receiveValue: { [weak self] fileTransferState in
guard let self = self else { return }
switch fileTransferState {
case .connected:
DLog("peripheral connencted")
case .disconnected:
if self.isDisconnectingPeripheral[fileTransferPeripheral.address] == true {
DLog("peripheral disconnected \(fileTransferPeripheral.address)")
self.removePeripheralFromAutomaticallyManagedConnection(address: fileTransferPeripheral.address)
self.fileTransferClients.removeValue(forKey: fileTransferPeripheral.address) // Remove info from disconnected peripheral
self.updateSelectedPeripheral()
}
else if self.isReconnectingPeripheral[fileTransferPeripheral.address] == true {
DLog("recover failed for \(fileTransferPeripheral.address)")
self.setReconnectionFailed(address: fileTransferPeripheral.address)
self.removePeripheralFromAutomaticallyManagedConnection(address: fileTransferPeripheral.address)
self.fileTransferClients.removeValue(forKey: fileTransferPeripheral.address) // Remove info from disconnected peripheral
self.updateSelectedPeripheral()
}
// If it was the selected peripheral -> try to recover the connection because a peripheral can be disconnected momentarily when writing to the filesystem.
else if self.currentFileTransferClient?.peripheral.address == fileTransferPeripheral.address {
if let selectedFileTransferClient = self.currentFileTransferClient {
self.userSelectedTransferClient = nil
// Wait for recovery before connecting to a different one
DLog("Try to recover disconnected peripheral: \(selectedFileTransferClient.peripheral.nameOrAddress)")
self.isReconnectingToBondedPeripherals = true
// Reconnect
self.isReconnectingPeripheral[fileTransferPeripheral.address] = true
fileTransferPeripheral.connectAndSetup(connectionTimeout: Self.kReconnectTimeout) { [weak self] result in
guard let self = self else { return }
DispatchQueue.main.async {
self.isReconnectingPeripheral[fileTransferPeripheral.address] = false
self.isReconnectingToBondedPeripherals = false
switch result {
case .success:
break
case .failure:
DLog("recover failed. Auto-select another peripheral")
self.removePeripheralFromAutomaticallyManagedConnection(address: fileTransferPeripheral.address)
self.fileTransferClients.removeValue(forKey: fileTransferPeripheral.address)
self.updateSelectedPeripheral()
}
}
}
}
}
case .error(_):
self.setReconnectionFailed(address: fileTransferPeripheral.address)
default:
break
}
})
managedBlePeripherals.append((fileTransferPeripheral, cancellable))
}
private func setReconnectionFailed(address: String) {
// If it the selectedPeripheral, then the reconnection failed
if currentFileTransferClient?.peripheral.address == address {
isReconnectingToBondedPeripherals = false
}
fileTransferClients.removeValue(forKey: address) // Remove info from disconnected peripheral
}
func clean() {
managedBlePeripherals.removeAll()
}
private func removePeripheralFromAutomaticallyManagedConnection(address: String) {
managedBlePeripherals.removeAll { (fileTransferPeripheral, _) in
fileTransferPeripheral.address == address
}
}
private func removePeripheralFromAutomaticallyManagedConnection(bleFileTransferPeripheral: KTBleFileTransferPeripheral) {
managedBlePeripherals.removeAll { (fileTransferPeripheral, _) in
fileTransferPeripheral.address == bleFileTransferPeripheral.address
}
}
}

View file

@ -0,0 +1,795 @@
//
// KTBleFileTransferPeripheral.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import CoreBluetooth
import Combine
class KTBleFileTransferPeripheral: KTFileTransferPeripheral {
// Constants
public static let kFileTransferServiceUUID = CBUUID(string: "FEBB")
private static let kFileTransferVersionCharacteristicUUID = CBUUID(string: "ADAF0100-4669-6C65-5472-616E73666572")
private static let kFileTransferDataCharacteristicUUID = CBUUID(string: "ADAF0200-4669-6C65-5472-616E73666572")
private static let kAdafruitDefaultVersion = 1
private static let kDebugMessagesEnabled = AppEnvironment.isDebug && true
// Data - Private
private static let readFileResponseHeaderSize = 16 // (1+1+2+4+4+4+variable)
private static let deleteFileResponseHeaderSize = 2 // (1+1)
private static let moveFileResponseHeaderSize = 2 // (1+1)
private static let writeFileResponseHeaderSize = 20 // (1+1+2+4+8+4)
private static let makeDirectoryResponseHeaderSize = 16 // (1+1+6+8)
private static let listDirectoryResponseHeaderSize = 28 // (1+1+2+4+4+4+8+4+variable)
private var fileTransferVersion: Int? = nil
private var dataCharacteristic: CBCharacteristic? = nil
private var dataProcessingQueue: KTDataProcessingQueue? = nil
private var readStatus: FileTransferReadStatus? = nil
private var writeStatus: FileTransferWriteStatus? = nil
private var deleteStatus: FileTransferDeleteStatus? = nil
private var listDirectoryStatus: FileTransferListDirectoryStatus? = nil
private var makeDirectoryStatus: FileTransferMakeDirectoryStatus? = nil
private var moveStatus: FileTransferMoveStatus? = nil
private var blePeripheral: KTBlePeripheral
private var onBonded: ((_ name: String, _ uuid: UUID) -> Void)?
enum BleFileTransferPeripheralError: Error {
case invalidCharacteristic
case enableNotifyFailed
case disableNotifyFailed
case unknownVersion
case invalidResponseData
}
// Data
var peripheral: KTPeripheral { blePeripheral }
var address: String { blePeripheral.address }
var nameOrAddress: String { blePeripheral.nameOrAddress }
// States
enum FileTransferState {
case start // Note: don't use disconnected as initial state to differentiate between a real disconnect and the initialization
case connecting
case connected
case disconnecting(error: Error? = nil)
case disconnected(error: Error? = nil)
case discovering
case checkingFileTransferVersion
case enablingNotifications
case enabled
case error(_ error: Error? = nil)
}
var fileTransferState = CurrentValueSubject<FileTransferState, Error>(.start)
private var stateObserverCancellable: Cancellable?
private var setupCompletion: ((Result<Void, Error>) -> Void)? = nil
// MARK: - Lifecycle
init(blePeripheral: KTBlePeripheral, onBonded: ((_ name: String, _ uuid: UUID) -> Void)?) {
self.blePeripheral = blePeripheral
self.onBonded = onBonded
self.stateUpdatesEnabled(true)
}
deinit {
DLog("BleFileTransferPeripheral deinit")
}
private func stateUpdatesEnabled(_ enabled: Bool) {
if enabled {
stateObserverCancellable = blePeripheral.state
.sink { [weak self] state in
guard let self = self else { return }
switch state {
case .connecting:
self.fileTransferState.value = .connecting
case .connected:
if let completion = self.setupCompletion {
self.fileTransferState.value = .discovering
// Discover
self.fileTransferEnable() { [weak self] result in
DLog("File Transfer Enable success: \(result.isSuccess)")
guard let self = self else { return }
self.setupCompletion = nil
switch result {
case .success:
self.fileTransferState.value = .enabled
completion(.success(()))
case .failure(let error):
self.fileTransferState.value = .error(error)
completion(.failure(error))
}
}
}
else {
self.fileTransferState.value = .connected
}
case .disconnecting:
self.fileTransferState.value = .disconnecting()
case .disconnected:
self.setupCompletion?(.failure(FileTransferError.disconnected))
self.disable()
self.fileTransferState.value = .disconnected()
@unknown default:
break
}
}
}
else {
stateObserverCancellable?.cancel()
stateObserverCancellable = nil
}
}
// MARK: - Actions
func connectAndSetup(connectionTimeout: TimeInterval?, completion: @escaping (Result<Void, Error>) -> Void) {
disable()
fileTransferState.value = .start
blePeripheral.connect(connectionTimeout: connectionTimeout) { [weak self] result in
guard let self = self else { return }
switch result {
case .success:
self.onBonded?(self.blePeripheral.nameOrAddress, self.blePeripheral.identifier)
self.setupCompletion = completion
case .failure:
completion(result)
}
}
}
// MARK: - Commands
func listDirectory(path: String, completion: ((Result<[KTDirectoryEntry]?, Error>) -> Void)?) {
if self.listDirectoryStatus != nil { DLog("Warning: concurrent listDirectory") }
self.listDirectoryStatus = FileTransferListDirectoryStatus(completion: completion)
let data = ([UInt8]([0x50, 0x00])).data
+ UInt16(path.utf8.count).littleEndian.data
+ Data(path.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
func makeDirectory(path: String, completion: ((Result<Date?, Error>) -> Void)?) {
self.makeDirectoryStatus = FileTransferMakeDirectoryStatus(completion: completion)
var data = ([UInt8]([0x40, 0x00])).data
+ UInt16(path.utf8.count).littleEndian.data
// Version 3 adds currentTime
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000*1000*1000)
data += ([UInt8]([0x00, 0x00, 0x00, 0x00])).data // 4 bytes padding
+ UInt64(currentTime).littleEndian.data
data += Data(path.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
func readFile(path: String, progress: KTFileTransferProgressHandler?, completion: ((Result<Data, Error>) -> Void)?) {
self.readStatus = FileTransferReadStatus(progress: progress, completion: completion)
let mtu = blePeripheral.maximumWriteValueLength(for: .withoutResponse)
let offset = 0
let chunkSize = mtu - Self.readFileResponseHeaderSize
let data = ([UInt8]([0x10, 0x00])).data
+ UInt16(path.utf8.count).littleEndian.data
+ UInt32(offset).littleEndian.data
+ UInt32(chunkSize).littleEndian.data
+ Data(path.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
func readFileChunk(offset: UInt32, chunkSize: UInt32, completion: ((Result<Void, Error>) -> Void)?) {
let data = ([UInt8]([0x12, 0x01, 0x00, 0x00])).data
+ UInt32(offset).littleEndian.data
+ UInt32(chunkSize).littleEndian.data
sendCommand(data: data, completion: completion)
}
func writeFile(path: String, data fileData: Data, progress: KTFileTransferProgressHandler?, completion: ((Result<Date?, Error>) -> Void)?) {
let fileStatus = FileTransferWriteStatus(data: fileData, progress: progress, completion: completion)
self.writeStatus = fileStatus
let offset = 0
let totalSize = fileStatus.data.count
var data = ([UInt8]([0x20, 0x00])).data
+ UInt16(path.utf8.count).littleEndian.data
+ UInt32(offset).littleEndian.data
let currentTime = UInt64(Date().timeIntervalSince1970 * 1000*1000*1000)
data += UInt64(currentTime).littleEndian.data
data += UInt32(totalSize).littleEndian.data
+ Data(path.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
// Note: uses info stored in adafruitFileTransferFileStatus to resume writing data
private func writeFileChunk(offset: UInt32, chunkSize: UInt32, completion: ((Result<Void, Error>) -> Void)?) {
guard let adafruitFileTransferWriteStatus = writeStatus else { completion?(.failure(FileTransferError.invalidInternalState)); return; }
let chunkData = adafruitFileTransferWriteStatus.data.subdata(in: Int(offset)..<(Int(offset)+Int(chunkSize)))
let data = ([UInt8]([0x22, 0x01, 0x00, 0x00])).data
+ UInt32(offset).littleEndian.data
+ UInt32(chunkSize).littleEndian.data
+ chunkData
if Self.kDebugMessagesEnabled {
DLog("write chunk at offset \(offset) chunkSize: \(chunkSize). message size: \(data.count). mtu: \(blePeripheral.maximumWriteValueLength(for: .withoutResponse))")
}
//DLog("\t\(String(data: chunkData, encoding: .utf8))")
sendCommand(data: data, completion: completion)
}
func deleteFile(path: String, completion: ((Result<Void, Error>) -> Void)?) {
self.deleteStatus = FileTransferDeleteStatus(completion: completion)
let data = ([UInt8]([0x30, 0x00])).data
+ UInt16(path.utf8.count).littleEndian.data
+ Data(path.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
func moveFile(fromPath: String, toPath: String, completion: ((Result<Void, Error>) -> Void)?) {
self.moveStatus = FileTransferMoveStatus(completion: completion)
let data = ([UInt8]([0x60, 0x00])).data
+ UInt16(fromPath.utf8.count).littleEndian.data
+ UInt16(toPath.utf8.count).littleEndian.data
+ Data(fromPath.utf8)
+ UInt8(0x00).data // Padding byte
+ Data(toPath.utf8)
sendCommand(data: data) { result in
if case .failure(let error) = result {
completion?(.failure(error))
}
}
}
private func sendCommand(data: Data, completion: ((Result<Void, Error>) -> Void)?) {
guard blePeripheral.state.value == .connected else {
completion?(.failure(FileTransferError.disconnected))
return
}
guard let adafruitFileTransferDataCharacteristic = dataCharacteristic else {
completion?(.failure(BleFileTransferPeripheralError.invalidCharacteristic))
return
}
blePeripheral.write(data: data, for: adafruitFileTransferDataCharacteristic, type: .withoutResponse) { error in
guard error == nil else {
completion?(.failure(error!))
return
}
completion?(.success(()))
}
}
// MARK: - File Transfer Management
private func fileTransferEnable(completion: ((Result<Void, Error>) -> Void)?) {
DLog("Discovering services...")
let serviceUuid = Self.kFileTransferServiceUUID
blePeripheral.characteristic(uuid: Self.kFileTransferDataCharacteristicUUID, serviceUuid: serviceUuid) { [weak self] (characteristic, error) in
guard let self = self else { return }
guard let characteristic = characteristic, error == nil else {
completion?(.failure(error ?? BleFileTransferPeripheralError.invalidCharacteristic))
return
}
// Check version
self.fileTransferState.value = .checkingFileTransferVersion
self.adafruitVersion(serviceUuid: serviceUuid, versionCharacteristicUUID: Self.kFileTransferVersionCharacteristicUUID) { [weak self] version in
guard let self = self else { return }
DLog("\(self.blePeripheral.nameOrAddress) FileTransfer Protocol v\(version) detected")
self.fileTransferVersion = version
self.dataCharacteristic = characteristic
// Set notify
self.fileTransferState.value = .enablingNotifications
self.adafruitServiceSetNotifyResponse(characteristic: characteristic, responseHandler: self.receiveFileTransferData, completion: completion)
}
}
}
func adafruitFileTransferIsEnabled() -> Bool {
return dataCharacteristic != nil && dataCharacteristic!.isNotifying
}
func disable() {
//statusUpdatesEnabled(false)
setupCompletion = nil
// Clear all internal data
fileTransferVersion = nil
dataCharacteristic = nil
dataProcessingQueue?.reset(forceReleaseLock: true)
dataProcessingQueue = nil
// Clear all internal variables for commands, sending an error to the completion handler if it was still executing
readStatus?.completion?(.failure(FileTransferError.disconnected))
readStatus = nil
writeStatus?.completion?(.failure(FileTransferError.disconnected))
writeStatus = nil
deleteStatus?.completion?(.failure(FileTransferError.disconnected))
deleteStatus = nil
listDirectoryStatus?.completion?(.failure(FileTransferError.disconnected))
listDirectoryStatus = nil
makeDirectoryStatus?.completion?(.failure(FileTransferError.disconnected))
makeDirectoryStatus = nil
moveStatus?.completion?(.failure(FileTransferError.disconnected))
moveStatus = nil
}
// MARK: - Receive Data
private func receiveFileTransferData(response: Result<(Data, UUID), Error>) {
switch response {
case .success(let (receivedData, peripheralIdentifier)):
// Init received data
if dataProcessingQueue == nil {
dataProcessingQueue = KTDataProcessingQueue(uuid: peripheralIdentifier)
}
processDataQueue(receivedData: receivedData)
case .failure(let error):
DLog("receiveFileTransferData error: \(error)")
}
}
private func processDataQueue(receivedData: Data) {
guard let adafruitFileTransferDataProcessingQueue = dataProcessingQueue else { return }
adafruitFileTransferDataProcessingQueue.processQueue(receivedData: receivedData) { remainingData in
return decodeResponseChunk(data: remainingData)
}
}
/// Returns number of bytes processed (they will need to be discarded from the queue)
// Note: Take into account that data can be a Data-slice
private func decodeResponseChunk(data: Data) -> Int {
var bytesProcessed = 0
guard let command = data.first else { DLog("Error: response invalid data"); return bytesProcessed }
//DLog("received command: \(command)")
switch command {
case 0x11:
bytesProcessed = decodeReadFile(data: data)
case 0x21:
bytesProcessed = decodeWriteFile(data: data)
case 0x31:
bytesProcessed = decodeDeleteFile(data: data)
case 0x41:
bytesProcessed = decodeMakeDirectory(data: data)
case 0x51:
bytesProcessed = decodeListDirectory(data: data)
case 0x61:
bytesProcessed = decodeMoveFile(data: data)
default:
DLog("Error: unknown command: \(KTHexUtils.hexDescription(bytes: [command], prefix: "0x")). Invalidating all received data...")
bytesProcessed = Int.max // Invalidate all received data
}
return bytesProcessed
}
private func decodeMoveFile(data: Data) -> Int {
guard let adafruitFileTransferMoveStatus = moveStatus else { DLog("Error: write invalid internal status. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferMoveStatus.completion
guard data.count >= Self.moveFileResponseHeaderSize else { return 0 } // Header has not been fully received yet
let status = data[1]
let isMoved = status == 0x01
self.writeStatus = nil
if isMoved {
completion?(.success(()))
}
else {
completion?(.failure(FileTransferError.statusFailed(code: Int(status))))
}
return Self.moveFileResponseHeaderSize // Return processed bytes
}
private func decodeWriteFile(data: Data) -> Int {
guard let adafruitFileTransferWriteStatus = writeStatus else { DLog("Error: write invalid internal status. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferWriteStatus.completion
guard data.count >= Self.writeFileResponseHeaderSize else { return 0 } // Header has not been fully received yet
var decodingOffset = 1
let status = data[decodingOffset]
let isStatusOk = status == 0x01
decodingOffset = 4 // Skip padding
let offset: UInt32 = data.scanValue(start: decodingOffset, length: 4)
decodingOffset += 4
var writeDate: Date? = nil
if fileTransferVersion ?? Self.kAdafruitDefaultVersion >= 3 {
let truncatedTime: UInt64 = data.scanValue(start: decodingOffset, length: 8)
writeDate = Date(timeIntervalSince1970: TimeInterval(truncatedTime)/(1000*1000*1000))
decodingOffset += 8
}
let freeSpace: UInt32 = data.scanValue(start: decodingOffset, length: 4)
if Self.kDebugMessagesEnabled {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd-HH:mm:ss"
DLog("write \(isStatusOk ? "ok":"error(\(status))") at offset: \(offset). \(writeDate == nil ? "" : "date: \(dateFormatter.string(from: writeDate!))") freespace: \(freeSpace)")
}
guard isStatusOk else {
self.writeStatus = nil
completion?(.failure(FileTransferError.statusFailed(code: Int(status))))
return Int.max // invalidate all received data on error
}
adafruitFileTransferWriteStatus.progress?(Int(offset), Int(adafruitFileTransferWriteStatus.data.count))
if offset >= adafruitFileTransferWriteStatus.data.count {
self.writeStatus = nil
completion?(.success((writeDate)))
}
else {
writeFileChunk(offset: offset, chunkSize: freeSpace) { result in
if case .failure(let error) = result {
self.writeStatus = nil
completion?(.failure(error))
}
}
}
return Self.writeFileResponseHeaderSize // Return processed bytes
}
/// Returns number of bytes processed
private func decodeReadFile(data: Data) -> Int {
guard let adafruitFileTransferReadStatus = readStatus else { DLog("Error: read invalid internal status. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferReadStatus.completion
guard data.count >= Self.readFileResponseHeaderSize else { return 0 } // Header has not been fully received yet
let status = data[1]
let isStatusOk = status == 0x01
let offset: UInt32 = data.scanValue(start: 4, length: 4)
let totalLength: UInt32 = data.scanValue(start: 8, length: 4)
let chunkSize: UInt32 = data.scanValue(start: 12, length: 4)
if Self.kDebugMessagesEnabled { DLog("read \(isStatusOk ? "ok":"error") at offset \(offset) chunkSize: \(chunkSize) totalLength: \(totalLength)") }
guard isStatusOk else {
self.readStatus = nil
completion?(.failure(FileTransferError.statusFailed(code: Int(status))))
return Int.max // invalidate all received data on error
}
let packetSize = Self.readFileResponseHeaderSize + Int(chunkSize)
guard data.count >= packetSize else { return 0 } // The first chunk is still no available wait for it
let chunkData = data.subdata(in: Self.readFileResponseHeaderSize..<packetSize)
self.readStatus!.data.append(chunkData)
adafruitFileTransferReadStatus.progress?(Int(offset + chunkSize), Int(totalLength))
if offset + chunkSize < totalLength {
let mtu = blePeripheral.maximumWriteValueLength(for: .withoutResponse)
let maxChunkLength = mtu - Self.readFileResponseHeaderSize
readFileChunk(offset: offset + chunkSize, chunkSize: UInt32(maxChunkLength)) { result in
if case .failure(let error) = result {
self.readStatus = nil
completion?(.failure(error))
}
}
}
else {
let fileData = self.readStatus!.data
self.readStatus = nil
completion?(.success(fileData))
}
return packetSize // Return processed bytes
}
private func decodeDeleteFile(data: Data) -> Int {
guard let adafruitFileTransferDeleteStatus = deleteStatus else {
DLog("Warning: unexpected delete result received. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferDeleteStatus.completion
guard data.count >= Self.deleteFileResponseHeaderSize else { return 0 } // Header has not been fully received yet
let status = data[1]
let isDeleted = status == 0x01
self.deleteStatus = nil
if isDeleted {
completion?(.success(()))
}
else {
completion?(.failure(FileTransferError.statusFailed(code: Int(status))))
}
return Self.deleteFileResponseHeaderSize // Return processed bytes
}
private func decodeMakeDirectory(data: Data) -> Int {
guard let adafruitFileTransferMakeDirectoryStatus = makeDirectoryStatus else { DLog("Warning: unexpected makeDirectory result received. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferMakeDirectoryStatus.completion
guard data.count >= Self.makeDirectoryResponseHeaderSize else { return 0 } // Header has not been fully received yet
let status = data[1]
let isCreated = status == 0x01
self.makeDirectoryStatus = nil
if isCreated {
var modificationDate: Date? = nil
let truncatedTime: UInt64 = data.scanValue(start: 8, length: 8)
modificationDate = Date(timeIntervalSince1970: TimeInterval(truncatedTime)/(1000*1000*1000))
completion?(.success(modificationDate))
}
else {
completion?(.failure(FileTransferError.statusFailed(code: Int(status))))
}
return Self.makeDirectoryResponseHeaderSize // Return processed bytes
}
private func decodeListDirectory(data: Data) -> Int {
guard let adafruitFileTransferListDirectoryStatus = listDirectoryStatus else {
DLog("Warning: unexpected list result received. Invalidating all received data..."); return Int.max }
let completion = adafruitFileTransferListDirectoryStatus.completion
let headerSize = Self.listDirectoryResponseHeaderSize
guard data.count >= headerSize else { return 0 } // Header has not been fully received yet
var packetSize = headerSize // Chunk size processed (can be less that data.count if several chunks are included in the data)
let directoryExists = data[data.startIndex + 1] == 0x1
if directoryExists, data.count >= headerSize {
let entryCount: UInt32 = data.scanValue(start: 8, length: 4)
if entryCount == 0 { // Empty directory
self.listDirectoryStatus = nil
completion?(.success([]))
}
else {
let pathLength: UInt16 = data.scanValue(start: 2, length: 2)
let entryIndex: UInt32 = data.scanValue(start: 4, length: 4)
if entryIndex >= entryCount { // Finished. Return entries
let entries = self.listDirectoryStatus!.entries
self.listDirectoryStatus = nil
if Self.kDebugMessagesEnabled { DLog("list: finished") }
completion?(.success(entries))
}
else {
let flags: UInt32 = data.scanValue(start: 12, length: 4)
let isDirectory = flags & 0x1 == 1
var decodingOffset = 16
var modificationDate: Date? = nil
let truncatedTime: UInt64 = data.scanValue(start: decodingOffset, length: 8)
modificationDate = Date(timeIntervalSince1970: TimeInterval(truncatedTime)/(1000*1000*1000))
decodingOffset += 8
let fileSize: UInt32 = data.scanValue(start: decodingOffset, length: 4) // Ignore for directories
guard data.count >= headerSize + Int(pathLength) else { return 0 } // Path is still no available wait for it
if pathLength > 0, let path = String(data: data[(data.startIndex + headerSize)..<(data.startIndex + headerSize + Int(pathLength))], encoding: .utf8) {
packetSize += Int(pathLength) // chunk includes the variable length path, so add it
if Self.kDebugMessagesEnabled {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd-HH:mm:ss"
DLog("list: \(entryIndex+1)/\(entryCount) \(isDirectory ? "directory":"file size: \(fileSize) bytes") \(modificationDate == nil ? "" : "date: \(dateFormatter.string(from: modificationDate!))"), path: '/\(path)'")
}
let entry = KTDirectoryEntry(name: path, type: isDirectory ? .directory : .file(size: Int(fileSize)), modificationDate: modificationDate)
// Add entry
self.listDirectoryStatus?.entries.append(entry)
}
else {
self.listDirectoryStatus = nil
completion?(.failure(FileTransferError.invalidData))
}
}
}
}
else {
self.listDirectoryStatus = nil
completion?(.success(nil)) // nil means directory does not exist
}
return packetSize
}
// MARK: - Service Utils
private func adafruitVersion(serviceUuid: CBUUID, versionCharacteristicUUID: CBUUID, completion: @escaping(Int) -> Void) {
blePeripheral.characteristic(uuid: versionCharacteristicUUID, serviceUuid: serviceUuid) { [weak self] (characteristic, error) in
guard let self = self else { return }
// Check if version characteristic exists or return default value
guard error == nil, let characteristic = characteristic else {
completion(Self.kAdafruitDefaultVersion)
return
}
// Read the version
self.blePeripheral.readCharacteristic(characteristic) { (result, error) in
guard error == nil, let data = result as? Data, data.count >= 4 else {
completion(Self.kAdafruitDefaultVersion)
return
}
let version = data.toIntFrom32Bits()
completion(version)
}
}
}
func adafruitServiceSetNotifyResponse(characteristic: CBCharacteristic, responseHandler: @escaping(Result<(Data, UUID), Error>) -> Void, completion: ((Result<Void, Error>) -> Void)?) {
// Prepare notification handler
let notifyHandler: ((Error?) -> Void)? = { [unowned self] error in
guard error == nil else {
responseHandler(.failure(error!))
return
}
if let data = characteristic.value {
responseHandler(.success((data, blePeripheral.identifier)))
}
}
// Enable notifications
if !characteristic.isNotifying {
blePeripheral.enableNotify(for: characteristic, handler: notifyHandler, completion: { error in
guard error == nil else {
completion?(.failure(error!))
return
}
guard characteristic.isNotifying else {
completion?(.failure(BleFileTransferPeripheralError.enableNotifyFailed))
return
}
completion?(.success(()))
})
} else {
blePeripheral.updateNotifyHandler(for: characteristic, handler: notifyHandler)
completion?(.success(()))
}
}
// MARK: - Data structures
private struct FileTransferReadStatus {
var data = Data()
var progress: KTFileTransferProgressHandler?
var completion: ((Result<Data, Error>) -> Void)?
}
private struct FileTransferWriteStatus {
var data: Data
var progress: KTFileTransferProgressHandler?
var completion: ((Result<Date?, Error>) -> Void)?
}
private struct FileTransferDeleteStatus {
var completion: ((Result<Void, Error>) -> Void)?
}
private struct FileTransferListDirectoryStatus {
var entries = [KTDirectoryEntry]()
var completion: ((Result<[KTDirectoryEntry]?, Error>) -> Void)?
}
private struct FileTransferMakeDirectoryStatus {
var completion: ((Result<Date?, Error>) -> Void)?
}
private struct FileTransferMoveStatus {
var completion: ((Result<Void, Error>) -> Void)?
}
// MARK: - Errors
public enum FileTransferError: LocalizedError {
case invalidData
case unknownCommand
case invalidInternalState
case statusFailed(code: Int)
case disconnected
public var errorDescription: String? {
switch self {
case .invalidData: return "invalid data"
case .unknownCommand: return "unknown command"
case .invalidInternalState: return "invalid internal state"
case .statusFailed(let code):
if code == 5 {
return "status error: \(code). Filesystem in read-only mode"
} else {
return "status error: \(code)"
}
case .disconnected: return "disconnected"
}
}
}
}

View file

@ -0,0 +1,101 @@
//
// KTDataProcessingQueue.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
class KTDataProcessingQueue {
private var data = Data()
private var dataSemaphore = DispatchSemaphore(value: 1)
private let uuid: UUID
init(uuid: UUID) {
self.uuid = uuid
}
/*
// MARK: - BLE Notifications
private weak var didConnectToPeripheralObserver: NSObjectProtocol?
private weak var didDisconnectFromPeripheralObserver: NSObjectProtocol?
private func registerNotifications(enabled: Bool) {
let notificationCenter = NotificationCenter.default
if enabled {
didConnectToPeripheralObserver = notificationCenter.addObserver(forName: .didConnectToPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didConnectToPeripheral(notification: notification)})
didDisconnectFromPeripheralObserver = notificationCenter.addObserver(forName: .didDisconnectFromPeripheral, object: nil, queue: .main, using: {[weak self] notification in self?.didDisconnectFromPeripheral(notification: notification)})
} else {
if let didConnectToPeripheralObserver = didConnectToPeripheralObserver {notificationCenter.removeObserver(didConnectToPeripheralObserver)}
if let didDisconnectFromPeripheralObserver = didDisconnectFromPeripheralObserver {notificationCenter.removeObserver(didDisconnectFromPeripheralObserver)}
}
}
private func didConnectToPeripheral(notification: Notification) {
guard let identifier = notification.userInfo?[NotificationUserInfoKey.uuid.rawValue] as? UUID else { return }
guard identifier == uuid else { return }
reset()
}
private func didDisconnectFromPeripheral(notification: Notification) {
guard let identifier = notification.userInfo?[NotificationUserInfoKey.uuid.rawValue] as? UUID else { return }
guard identifier == uuid else { return }
reset(forceReleaseLock: true)
}*/
func reset(forceReleaseLock: Bool = false) {
// Clear cached data
data.removeAll()
if forceReleaseLock {
// Force signal if it was waiting
dataSemaphore.signal()
}
}
func processQueue(receivedData: Data, processingHandler: ((Data)->Int)) {
// Don't append more data until the delegate has finished processing it
dataSemaphore.wait()
// Append received data
data.append(receivedData)
//DLog("Data received. Queue size: \(data.count) bytes")
// Process chunks
processQueuedChunks(processingHandler: processingHandler)
// Ready to receive more data
dataSemaphore.signal()
}
// Important: this method changes "data", so it should be used only when the semaphore is blocking concurrent access
private func processQueuedChunks(processingHandler: ((Data)->Int)) {
// Process chunk
let processedDataCount = processingHandler(data)
// Remove processed bytes
if processedDataCount > 0 {
data = Data(data.dropFirst(processedDataCount))
}
else {
//DLog("Queue size: \(data.count) bytes. Waiting for more data to process packet...")
}
// If there is still unprocessed chunks in the queue, process the next one
let isStillUnprocessedDataInQueue = processedDataCount > 0 && data.count > 0
if isStillUnprocessedDataInQueue {
//DLog("Unprocessed data still in queue (\(data.count) bytes). Try to process next packet")
processQueuedChunks(processingHandler: processingHandler)
}/*
else if data.isEmpty {
DLog("Data queue empty")
}*/
}
}

View file

@ -0,0 +1,36 @@
//
// KTDirectoryEntry.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
public struct KTDirectoryEntry: Codable {
public enum EntryType: Codable {
case file(size: Int)
case directory
}
public let name: String
public let type: EntryType
public let modificationDate: Date?
public init(name: String, type: EntryType, modificationDate: Date?) {
self.name = name
self.type = type
self.modificationDate = modificationDate
}
public var isDirectory: Bool {
switch type {
case .directory: return true
default: return false
}
}
public var isHidden: Bool {
return name.starts(with: ".")
}
}

View file

@ -0,0 +1,52 @@
//
// KTFileTransferClient.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
class KTFileTransferClient {
let fileTransferPeripheral: KTFileTransferPeripheral
var peripheral: KTPeripheral { fileTransferPeripheral.peripheral }
init(fileTransferPeripheral: KTFileTransferPeripheral) {
self.fileTransferPeripheral = fileTransferPeripheral
}
// MARK: - File Transfer Commands
/// Given a full path, returns the full contents of the file
public func readFile(path: String, progress: KTFileTransferProgressHandler? = nil, completion: ((Result<Data, Error>) -> Void)?) {
fileTransferPeripheral.readFile(path: path, progress: progress, completion: completion)
}
/// Writes the content to the given full path. If the file exists, it will be overwritten
public func writeFile(path: String, data: Data, progress: KTFileTransferProgressHandler? = nil, completion: ((Result<Date?, Error>) -> Void)?) {
fileTransferPeripheral.writeFile(path: path, data: data, progress: progress, completion: completion)
}
/// Deletes the file or directory at the given full path. Directories must be empty to be deleted
public func deleteFile(path: String, completion: ((Result<Void, Error>) -> Void)?) {
fileTransferPeripheral.deleteFile(path: path, completion: completion)
}
/**
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
- Parameter path: Full path
*/
public func makeDirectory(path: String, completion: ((Result<Date?, Error>) -> Void)?) {
fileTransferPeripheral.makeDirectory(path: FileTransferPathUtils.pathWithTrailingSeparator(path: path), completion: completion)
}
/// Lists all of the contents in a directory given a full path. Returned paths are relative to the given path to reduce duplication
public func listDirectory(path: String, completion: ((Result<[KTDirectoryEntry]?, Error>) -> Void)?) {
fileTransferPeripheral.listDirectory(path: path, completion: completion)
}
/// Moves a single file from fromPath to toPath
public func moveFile(fromPath: String, toPath: String, completion: ((Result<Void, Error>) -> Void)?) {
fileTransferPeripheral.moveFile(fromPath: fromPath, toPath: toPath, completion: completion)
}
}

View file

@ -0,0 +1,53 @@
//
// KTFileTransferPeripheral.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
public typealias KTFileTransferProgressHandler = ((_ transmittedBytes: Int, _ totalBytes: Int) -> Void)
protocol KTFileTransferPeripheral {
var peripheral: KTPeripheral { get }
func connectAndSetup(
connectionTimeout: TimeInterval?,
completion: @escaping (Result<Void, Error>) -> Void
)
func listDirectory(
path: String,
completion: ((Result<[KTDirectoryEntry]?, Error>) -> Void)?
)
func makeDirectory(
path: String,
completion: ((Result<Date?, Error>) -> Void)?
)
func readFile(
path: String,
progress: KTFileTransferProgressHandler?,
completion: ((Result<Data, Error>) -> Void)?
)
func writeFile(
path: String,
data: Data,
progress: KTFileTransferProgressHandler?,
completion: ((Result<Date?, Error>) -> Void)?
)
func deleteFile(
path: String,
completion: ((Result<Void, Error>) -> Void)?
)
func moveFile(
fromPath: String,
toPath: String,
completion: ((Result<Void, Error>) -> Void)?
)
}

View file

@ -0,0 +1,65 @@
//
// KTScanner.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
import Combine
class Scanner: ObservableObject {
private let blePeripheralScanner: any KTBlePeripheralScanner
private var scannedBlePeripherals = [KTBlePeripheral]()
private var disposables = Set<AnyCancellable>()
enum ScanningState {
case idle
case scanning(peripherals: [KTPeripheral])
//case scanningError(error: Error)
var isScanning: Bool {
switch self {
case .scanning: return true
default: return false
}
}
}
@Published var scanningState: ScanningState = .idle
var bleLastErrorPublisher: Published<Error?>.Publisher
init(blePeripheralScanner: any KTBlePeripheralScanner) {
self.blePeripheralScanner = blePeripheralScanner
// Map errors
bleLastErrorPublisher = blePeripheralScanner.bleLastErrorPublisher
// Map BLE peripherals
blePeripheralScanner.blePeripheralsPublisher
//.receive(on: RunLoop.main)
.sink { blePeripherals in
let filteredPeripherals = blePeripherals
.filter({/*$0.advertisement.isManufacturerAdafruit() &&*/ $0.advertisement.services?.contains(KTBleFileTransferPeripheral.kFileTransferServiceUUID) ?? false})
.sorted(by: { $0.createdTime < $1.createdTime })
self.scannedBlePeripherals = filteredPeripherals
self.updateScanningState()
}
.store(in: &disposables)
}
func start() {
blePeripheralScanner.start()
}
func stop() {
blePeripheralScanner.stop()
}
private func updateScanningState() {
let allPeripherals: [KTPeripheral] = scannedBlePeripherals
scanningState = .scanning(peripherals: allPeripherals)
}
}

View file

@ -0,0 +1,28 @@
//
// LogHelper.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
// Note: check that Build Settings -> Project -> Active Compilation Conditions -> Debug, has DEBUG
public func DLog(_ message: String, function: String = #function) {
if _isDebugAssertConfiguration() {
NSLog("%@, %@", function, message)
}
// Send notification in case we are using LogManager
NotificationCenter.default.post(name: .didLogDebugMessage, object: nil, userInfo: ["message" : message])
}
// MARK: - Custom Notifications
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
public static let didLogDebugMessage = Notification.Name(kPrefix+".didLogDebugMessage")
}

View file

@ -0,0 +1,7 @@
# Updated AdafruitKit
The updated AdafruitKit will be merged in with the existing and obsolete "AdafruitKit".
We'll slowly need to replace the old version with the newer version that uses "Combine".
The updated framework will use the "KT" prefix.

View file

@ -0,0 +1,60 @@
//
// KTCommandQueue.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
// Command array, executed sequencially
class KTCommandQueue<Element> {
let executeHandler: ((_ command: Element) -> Void)?
private var queueLock = NSLock()
init(executeHandler: ((_ command: Element) -> Void)?) {
self.executeHandler = executeHandler
}
var queue = [Element]()
func first() -> Element? {
queueLock.lock(); defer { queueLock.unlock() }
//DLog("queue: \(queue) first: \(queue.first)")
return queue.first
}
func executeNext() {
queueLock.lock()
guard !queue.isEmpty else { queueLock.unlock(); return }
//DLog("queue remove finished: \(queue.first)")
// Delete finished command and trigger next execution if needed
queue.removeFirst()
let nextElement = queue.first
queueLock.unlock()
if let nextElement = nextElement {
//DLog("execute next")
executeHandler?(nextElement)
}
}
func append(_ element: Element) {
queueLock.lock()
let shouldExecute = queue.isEmpty
queue.append(element)
queueLock.unlock()
//DLog("queue: \(queue) append: \(element). total: \(queue.count)")
if shouldExecute {
executeHandler?(element)
}
}
func removeAll() {
// DLog("queue removeAll: \(queue.count)")
queue.removeAll()
}
}

View file

@ -0,0 +1,79 @@
//
// KTData+LittleEndianTypes.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
extension Data {
func toFloatFrom32Bits() -> Float {
return Float(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) }))
}
func toIntFrom32Bits() -> Int {
return Int(Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) })))
}
func toInt32From32Bits() -> Int32 {
return Int32(bitPattern: UInt32(littleEndian: self.withUnsafeBytes { $0.load(as: UInt32.self) }))
}
}
struct KTHexUtils {
static func hexDescription(data: Data, prefix: String = "", postfix: String = " ") -> String {
return data.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)}
}
static func hexDescription(bytes: [UInt8], prefix: String = "", postfix: String = " ") -> String {
return bytes.reduce("") {$0 + String(format: "%@%02X%@", prefix, $1, postfix)}
}
static func decimalDescription(data: Data, prefix: String = "", postfix: String = " ") -> String {
return data.reduce("") {$0 + String(format: "%@%ld%@", prefix, $1, postfix)}
}
}
protocol UIntToBytesConvertable {
var toBytes: [UInt8] { get }
}
extension UIntToBytesConvertable {
fileprivate func toByteArr<T: FixedWidthInteger>(endian: T, count: Int) -> [UInt8] {
var _endian = endian
let bytePtr = withUnsafePointer(to: &_endian) {
$0.withMemoryRebound(to: UInt8.self, capacity: count) {
UnsafeBufferPointer(start: $0, count: count)
}
}
return [UInt8](bytePtr)
}
}
extension UInt16: UIntToBytesConvertable {
var toBytes: [UInt8] {
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt16>.size)
}
}
extension UInt32: UIntToBytesConvertable {
var toBytes: [UInt8] {
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt32>.size)
}
}
extension UInt64: UIntToBytesConvertable {
var toBytes: [UInt8] {
return toByteArr(endian: self.littleEndian, count: MemoryLayout<UInt64>.size)
}
}
// MARK: - Data Scan
extension Data {
func scanValue<T>(start: Int, length: Int) -> T {
let subdata = self.subdata(in: (self.startIndex + start)..<(self.startIndex + start + length))
return subdata.withUnsafeBytes { $0.load(as: T.self) }
}
}

View file

@ -0,0 +1,78 @@
//
// KTFileTransferPathUtils.swift
// PyLeap
//
// Created by Trevor Beaton on 4/19/24.
//
import Foundation
// 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
public struct KTFileTransferPathUtils {
static let pathSeparatorCharacter: Character = "/"
public static let pathSeparator = String(pathSeparatorCharacter)
// MARK: - Path management
public static func pathRemovingFilename(path: String) -> String {
guard let filenameIndex = path.lastIndex(of: Self.pathSeparatorCharacter) else {
return path
}
return String(path[path.startIndex...filenameIndex])
}
public static func filenameFromPath(path: String) -> String {
guard let filenameIndex = path.lastIndex(of: Self.pathSeparatorCharacter) else {
return path
}
return String(String(path[filenameIndex...]).dropFirst())
}
public static func upPath(from path: String) -> String {
// Remove trailing separator if exists
let filenamePath: String
if path.last == Self.pathSeparatorCharacter {
filenamePath = String(path.dropLast())
}
else {
filenamePath = path
}
// Remove any filename
let pathWithoutFilename = FileTransferPathUtils.pathRemovingFilename(path: filenamePath)
return pathWithoutFilename
}
public static func parentPath(from path: String) -> String {
guard !isRootDirectory(path: path) else { return rootDirectory } // Root parent is root
let parentPath: String
// Remove leading '/' and find the next one. Keep anything after the one found
let pathWithoutLeadingSlash = path.deletingPrefix(rootDirectory)
if let indexOfLastSlash = pathWithoutLeadingSlash.lastIndex(of: pathSeparatorCharacter) {
let parentPathWithoutLeadingSlash = String(pathWithoutLeadingSlash.prefix(upTo: indexOfLastSlash))
parentPath = rootDirectory + parentPathWithoutLeadingSlash
}
else { // Is root (only the leading '/' found)
parentPath = rootDirectory
}
return parentPath
}
public static func pathWithTrailingSeparator(path: String) -> String {
return path.hasSuffix(Self.pathSeparator) ? path : path.appending(Self.pathSeparator) // Force a trailing separator
}
// MARK: - Root Directory
public static var rootDirectory: String {
return Self.pathSeparator
}
public static func isRootDirectory(path: String) -> Bool {
return path == rootDirectory
}
}

Some files were not shown because too many files have changed in this diff Show more