Compare commits
9 commits
master
...
FileTransf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c2832807 | ||
|
|
57b1eced76 | ||
|
|
7e2ee8733b | ||
|
|
51e7ef0573 | ||
|
|
2ecc9a2ec9 | ||
|
|
0646453175 | ||
|
|
a34e2bf6d3 | ||
|
|
e5bb6df771 | ||
|
|
976bf512fa |
106 changed files with 4536 additions and 1310 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
};
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
21
PyLeap/Assets.xcassets/Placeholder Board Image.imageset/Contents.json
vendored
Normal file
21
PyLeap/Assets.xcassets/Placeholder Board Image.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
PyLeap/Assets.xcassets/Placeholder Board Image.imageset/Default device image.png
vendored
Normal file
BIN
PyLeap/Assets.xcassets/Placeholder Board Image.imageset/Default device image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>")")
|
||||
}
|
||||
}
|
||||
47
PyLeap/Features/Paired View/BleBannerView.swift
Normal file
47
PyLeap/Features/Paired View/BleBannerView.swift
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -54,8 +54,7 @@ struct RootView: View {
|
|||
case .selection:
|
||||
SelectionView()
|
||||
|
||||
case .wifiSelection:
|
||||
WifiSelection()
|
||||
|
||||
|
||||
case .wifiPairingTutorial:
|
||||
WifiPairingView()
|
||||
|
|
@ -47,7 +47,7 @@ public class RootViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
func goToWiFiSelection() {
|
||||
destination = .wifiSelection
|
||||
destination = .wifiServiceSelection
|
||||
}
|
||||
|
||||
func goToWifiView() {
|
||||
|
|
@ -16,8 +16,6 @@ struct WifiHeaderView: View {
|
|||
|
||||
VStack {
|
||||
|
||||
|
||||
|
||||
HStack (alignment: .center, spacing: 0) {
|
||||
|
||||
Image(systemName: "gearshape")
|
||||
143
PyLeap/Features/Unpaired View/MainSelectionView.swift
Normal file
143
PyLeap/Features/Unpaired View/MainSelectionView.swift
Normal 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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ struct WifiCell: View {
|
|||
|
||||
@EnvironmentObject var expandedState : ExpandedState
|
||||
|
||||
let result : ResultItem
|
||||
let result : PyProject
|
||||
|
||||
@State var isExpanded: Bool = false {
|
||||
didSet {
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
99
PyLeap/Features/Wifi Module/WifiServiceCellSubView.swift
Normal file
99
PyLeap/Features/Wifi Module/WifiServiceCellSubView.swift
Normal 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)
|
||||
// }
|
||||
//}
|
||||
81
PyLeap/Features/Wifi Module/WifiServiceCellView.swift
Normal file
81
PyLeap/Features/Wifi Module/WifiServiceCellView.swift
Normal 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() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
// PyLeap
|
||||
//
|
||||
// Created by Trevor Beaton on 10/24/22.
|
||||
//
|
||||
// Testing
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -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")
|
||||
// }
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
|
@ -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)")
|
||||
|
|
@ -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======")
|
||||
|
||||
53
PyLeap/Helpers/BoardDataProvider.swift
Normal file
53
PyLeap/Helpers/BoardDataProvider.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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() -> ()){
|
||||
55
PyLeap/Updated AdafruitKit/KTBle/KTBleManager.swift
Normal file
55
PyLeap/Updated AdafruitKit/KTBle/KTBleManager.swift
Normal 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")
|
||||
|
||||
}
|
||||
115
PyLeap/Updated AdafruitKit/KTBle/KTBleManagerImpl.swift
Normal file
115
PyLeap/Updated AdafruitKit/KTBle/KTBleManagerImpl.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
19
PyLeap/Updated AdafruitKit/KTBle/KTPeripheral.swift
Normal file
19
PyLeap/Updated AdafruitKit/KTBle/KTPeripheral.swift
Normal 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()
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
430
PyLeap/Updated AdafruitKit/KTConnectionManager.swift
Normal file
430
PyLeap/Updated AdafruitKit/KTConnectionManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -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: ".")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)?
|
||||
)
|
||||
}
|
||||
65
PyLeap/Updated AdafruitKit/KTFileTransfer/KTScanner.swift
Normal file
65
PyLeap/Updated AdafruitKit/KTFileTransfer/KTScanner.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
28
PyLeap/Updated AdafruitKit/LogHelper.swift
Normal file
28
PyLeap/Updated AdafruitKit/LogHelper.swift
Normal 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")
|
||||
|
||||
}
|
||||
7
PyLeap/Updated AdafruitKit/Note.md
Normal file
7
PyLeap/Updated AdafruitKit/Note.md
Normal 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.
|
||||
|
||||
60
PyLeap/Updated AdafruitKit/Utils/KTCommandQueue.swift
Normal file
60
PyLeap/Updated AdafruitKit/Utils/KTCommandQueue.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue