diff --git a/.gitmodules b/.gitmodules index 78c717a7..09dbc9ac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,9 +4,6 @@ [submodule "phyphox-webinterface"] path = phyphox-webinterface url = https://github.com/Kuhlen/phyphox-webinterface -[submodule "phyphox-iOS/CocoaMQTT"] - path = phyphox-iOS/CocoaMQTT - url = https://github.com/phyphox/CocoaMQTT.git [submodule "phyphox-iOS/ZipZap"] path = phyphox-iOS/ZipZap url = https://github.com/pixelglow/ZipZap.git diff --git a/phyphox-iOS/phyphox.xcodeproj/project.pbxproj b/phyphox-iOS/phyphox.xcodeproj/project.pbxproj index 3a0b2faa..cccc902a 100644 --- a/phyphox-iOS/phyphox.xcodeproj/project.pbxproj +++ b/phyphox-iOS/phyphox.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -155,9 +155,25 @@ 6BEA94592080B2F30077274A /* MultilineTextElementHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEA94582080B2F30077274A /* MultilineTextElementHandler.swift */; }; 6BF82BC82147FF6F00175659 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF82BC72147FF6F00175659 /* SemanticVersion.swift */; }; 6BFE372C20442AB400301CA3 /* KeyboardTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BFE372B20442AB400301CA3 /* KeyboardTracker.swift */; }; + 860E13F32B514703008FAFC6 /* GCDWebServer in Frameworks */ = {isa = PBXBuildFile; productRef = 860E13F22B514703008FAFC6 /* GCDWebServer */; }; 866632E92A29E1D90068762D /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866632E82A29E1D90068762D /* UIColorExtensions.swift */; }; + 867E2BD82AC464D000ABB472 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = 867E2BD72AC464D000ABB472 /* CocoaMQTT */; }; + A101AB0829BB801600B82311 /* ConnectedBluetoothDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A101AB0729BB801600B82311 /* ConnectedBluetoothDeviceViewController.swift */; }; + A14B3A472B7AD2960016E380 /* ExperimentCameraInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14B3A462B7AD2960016E380 /* ExperimentCameraInput.swift */; }; + A15891F02B10BFD000A169C3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15891EF2B10BFD000A169C3 /* MetalView.swift */; }; + A15891F22B10BFED00A169C3 /* MetalRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15891F12B10BFED00A169C3 /* MetalRenderer.swift */; }; + A182CEDD2B025B23000385D2 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182CEDC2B025B23000385D2 /* CameraView.swift */; }; + A182CEDF2B025B41000385D2 /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182CEDE2B025B41000385D2 /* CameraModel.swift */; }; + A182CEE12B025B53000385D2 /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182CEE02B025B53000385D2 /* CameraService.swift */; }; + A182CEE32B025B6C000385D2 /* CameraInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182CEE22B025B6C000385D2 /* CameraInput.swift */; }; + A182CEE72B02DF10000385D2 /* CameraService+Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182CEE62B02DF10000385D2 /* CameraService+Enums.swift */; }; + A18E13582B7CAFF4006EB323 /* ExperimentCameraInputSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A18E13572B7CAFF4006EB323 /* ExperimentCameraInputSession.swift */; }; + A18E135A2B7F563B006EB323 /* CameraUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A18E13592B7F563B006EB323 /* CameraUIView.swift */; }; A1A6A72F297685DF00090309 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A6A72E297685DF00090309 /* Utility.swift */; }; A1A80D3D29AE049E00AD756D /* SettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A80D3C29AE049E00AD756D /* SettingsBundleHelper.swift */; }; + A1B9C1BE2AA9E59D00238AF6 /* CameraViewDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B9C1BD2AA9E59D00238AF6 /* CameraViewDescriptor.swift */; }; + A1B9C1C02AA9E76C00238AF6 /* CameraViewElementHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B9C1BF2AA9E76C00238AF6 /* CameraViewElementHandler.swift */; }; + A1B9C1C42AA9EC9600238AF6 /* ExperimentCameraGUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B9C1C32AA9EC9600238AF6 /* ExperimentCameraGUIView.swift */; }; A1E03A7F29B61A9000CE93A6 /* UIImageExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E03A7E29B61A9000CE93A6 /* UIImageExt.swift */; }; A1FA9A00299FCFBC00E0FDBF /* color.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1FA99FF299FCFBC00E0FDBF /* color.xcassets */; }; A1FA9A0229A5309200E0FDBF /* PhyphoxUIAlertBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FA9A0129A5309200E0FDBF /* PhyphoxUIAlertBuilder.swift */; }; @@ -203,7 +219,6 @@ C07B530D22942CC300BA085A /* InputConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07B530C22942CC300BA085A /* InputConversion.swift */; }; C07BBC1F26209CAC0097C57A /* ExperimentTimedRunDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07BBC1E26209CAC0097C57A /* ExperimentTimedRunDialogView.swift */; }; C0804DC52471C48600AD32F4 /* CRC32InputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0804DC42471C48600AD32F4 /* CRC32InputStream.swift */; }; - C086C6F22707560100348772 /* CocoaMQTT.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C086C6EA270755E100348772 /* CocoaMQTT.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C08725112241460500079279 /* ColorElementHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08725102241460500079279 /* ColorElementHandler.swift */; }; C08F3909218AFB0800AB5CE0 /* LegacyStateSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08F3908218AFB0800AB5CE0 /* LegacyStateSerializer.swift */; }; C0914ED922688F1E00000A2F /* FormulaParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0914ED822688F1E00000A2F /* FormulaParser.swift */; }; @@ -219,11 +234,9 @@ C0AD8542229E7E7A00421738 /* MenuTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AD8541229E7E7A00421738 /* MenuTableViewController.swift */; }; C0AD8552229EA1CF00421738 /* HintBubbleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AD8551229EA1CF00421738 /* HintBubbleViewController.swift */; }; C0AE8A982294558000A3EC48 /* ConfigConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AE8A972294558000A3EC48 /* ConfigConversion.swift */; }; - C0BD4DEA27079C0D0020A5AF /* CocoaMQTT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C086C6EA270755E100348772 /* CocoaMQTT.framework */; }; C0BD4DF627079C600020A5AF /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = C0BD4DF527079C600020A5AF /* JGProgressHUD */; }; C0C529002254C01E001E5EFD /* ReduceAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C528FF2254C01E001E5EFD /* ReduceAnalysis.swift */; }; C0C529102254F2F3001E5EFD /* MapAnalysis.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C5290F2254F2F3001E5EFD /* MapAnalysis.swift */; }; - C0F430C328E5A16D00BC98DE /* GCDWebServers in Frameworks */ = {isa = PBXBuildFile; productRef = C0F430C228E5A16D00BC98DE /* GCDWebServers */; }; C0F430C628E5A2DB00BC98DE /* BEMCheckBox in Frameworks */ = {isa = PBXBuildFile; productRef = C0F430C528E5A2DB00BC98DE /* BEMCheckBox */; }; C0FC14872707C16300512064 /* libJXLS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0FC14862707C15200512064 /* libJXLS.a */; }; C0FC14A12707C1AE00512064 /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0FC14932707C19900512064 /* ZipZap.framework */; }; @@ -245,34 +258,6 @@ remoteGlobalIDString = 6B7BF7791C130425007408FB; remoteInfo = phyphox; }; - C086C6E9270755E100348772 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 8225B532227C2E8700E4DB51; - remoteInfo = "iOS CocoaMQTT"; - }; - C086C6EB270755E100348772 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 8225B54C227C32C500E4DB51; - remoteInfo = "CocoaMQTT-Tests"; - }; - C086C6ED270755E100348772 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 111124B224BB515E00E2DFBC; - remoteInfo = "Mac CocoaMQTT"; - }; - C086C6EF270755E100348772 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 111124DC24BB516400E2DFBC; - remoteInfo = "tvOS CocoaMQTT"; - }; C0FC14852707C15200512064 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C0FC14812707C15200512064 /* JXLSiOS.xcodeproj */; @@ -339,7 +324,6 @@ dstSubfolderSpec = 10; files = ( C0FC14A22707C1AE00512064 /* ZipZap.framework in Embed Frameworks */, - C086C6F22707560100348772 /* CocoaMQTT.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -513,8 +497,22 @@ 86C183D029F131BC0089E396 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/InfoPlist.strings; sourceTree = ""; }; 86C183D129F132020089E396 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Localizable.strings; sourceTree = ""; }; 86C183D229F132020089E396 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/InfoPlist.strings; sourceTree = ""; }; + A101AB0729BB801600B82311 /* ConnectedBluetoothDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedBluetoothDeviceViewController.swift; sourceTree = ""; }; + A14B3A462B7AD2960016E380 /* ExperimentCameraInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentCameraInput.swift; sourceTree = ""; }; + A15891EF2B10BFD000A169C3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; + A15891F12B10BFED00A169C3 /* MetalRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalRenderer.swift; sourceTree = ""; }; + A182CEDC2B025B23000385D2 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; + A182CEDE2B025B41000385D2 /* CameraModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = ""; }; + A182CEE02B025B53000385D2 /* CameraService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; + A182CEE22B025B6C000385D2 /* CameraInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraInput.swift; sourceTree = ""; }; + A182CEE62B02DF10000385D2 /* CameraService+Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraService+Enums.swift"; sourceTree = ""; }; + A18E13572B7CAFF4006EB323 /* ExperimentCameraInputSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentCameraInputSession.swift; sourceTree = ""; }; + A18E13592B7F563B006EB323 /* CameraUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraUIView.swift; sourceTree = ""; }; A1A6A72E297685DF00090309 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; A1A80D3C29AE049E00AD756D /* SettingsBundleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBundleHelper.swift; sourceTree = ""; }; + A1B9C1BD2AA9E59D00238AF6 /* CameraViewDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewDescriptor.swift; sourceTree = ""; }; + A1B9C1BF2AA9E76C00238AF6 /* CameraViewElementHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewElementHandler.swift; sourceTree = ""; }; + A1B9C1C32AA9EC9600238AF6 /* ExperimentCameraGUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentCameraGUIView.swift; sourceTree = ""; }; A1E03A7E29B61A9000CE93A6 /* UIImageExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExt.swift; sourceTree = ""; }; A1FA99FF299FCFBC00E0FDBF /* color.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = color.xcassets; sourceTree = ""; }; A1FA9A0129A5309200E0FDBF /* PhyphoxUIAlertBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhyphoxUIAlertBuilder.swift; sourceTree = ""; }; @@ -584,7 +582,6 @@ C07B530C22942CC300BA085A /* InputConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConversion.swift; sourceTree = ""; }; C07BBC1E26209CAC0097C57A /* ExperimentTimedRunDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentTimedRunDialogView.swift; sourceTree = ""; }; C0804DC42471C48600AD32F4 /* CRC32InputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CRC32InputStream.swift; sourceTree = ""; }; - C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = CocoaMQTT.xcodeproj; path = CocoaMQTT/CocoaMQTT.xcodeproj; sourceTree = ""; }; C08725102241460500079279 /* ColorElementHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorElementHandler.swift; sourceTree = ""; }; C08F3908218AFB0800AB5CE0 /* LegacyStateSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyStateSerializer.swift; sourceTree = ""; }; C0914ED822688F1E00000A2F /* FormulaParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormulaParser.swift; sourceTree = ""; }; @@ -620,13 +617,13 @@ C0FC14A12707C1AE00512064 /* ZipZap.framework in Frameworks */, 6B0815441CABF61B00EE2791 /* libc++.tbd in Frameworks */, C06BCD372707AAFE00B29595 /* libz.tbd in Frameworks */, - C0BD4DEA27079C0D0020A5AF /* CocoaMQTT.framework in Frameworks */, + 867E2BD82AC464D000ABB472 /* CocoaMQTT in Frameworks */, C06BCD4D2707B09700B29595 /* ImageIO.framework in Frameworks */, + 860E13F32B514703008FAFC6 /* GCDWebServer in Frameworks */, C0F430C628E5A2DB00BC98DE /* BEMCheckBox in Frameworks */, C0BD4DF627079C600020A5AF /* JGProgressHUD in Frameworks */, C0FC14872707C16300512064 /* libJXLS.a in Frameworks */, C06BCD4C2707B07100B29595 /* MobileCoreServices.framework in Frameworks */, - C0F430C328E5A16D00BC98DE /* GCDWebServers in Frameworks */, C06BCD4F2707B09C00B29595 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -662,6 +659,7 @@ 4BE7CAF81E4CD73D00C31090 /* ExperimentSeparatorView.swift */, 4B5568B41DD7D1F4000C033E /* ExperimentButtonView.swift */, C034B0102716F9E500C0EF00 /* ExperimentDepthGUIView.swift */, + A1B9C1C32AA9EC9600238AF6 /* ExperimentCameraGUIView.swift */, C034B02227171E0100C0EF00 /* DepthGUI */, ); path = Static; @@ -759,6 +757,7 @@ 6B7BF77C1C130425007408FB /* phyphox */ = { isa = PBXGroup; children = ( + A182CEDB2B025AE8000385D2 /* Camera */, A1E03AA129B779A400CE93A6 /* Helper */, 6B7BF7C11C13048B007408FB /* AppDelegate.swift */, 6B0C71541C1C706A00500FEE /* Constants.swift */, @@ -802,6 +801,7 @@ 6BEA9454207FE8990077274A /* ButtonViewElementHandler.swift */, 6BEA9456207FF8CE0077274A /* GraphViewElementHandler.swift */, C034B01E2716FBC800C0EF00 /* DepthGUIViewElementHandler.swift */, + A1B9C1BF2AA9E76C00238AF6 /* CameraViewElementHandler.swift */, ); path = ViewHandlers; sourceTree = ""; @@ -966,6 +966,7 @@ 6B26F6FC1CAAEDFB00EA9367 /* ExperimentExportSetSelectionView.swift */, C07BBC1E26209CAC0097C57A /* ExperimentTimedRunDialogView.swift */, 6B2E0F9E20593A01008FAA91 /* ViewModules */, + A101AB0729BB801600B82311 /* ConnectedBluetoothDeviceViewController.swift */, ); path = ExperimentView; sourceTree = ""; @@ -982,6 +983,7 @@ C034B0202716FDE400C0EF00 /* DepthGUIDescriptor.swift */, 4BE7CADF1E4CD20F00C31090 /* SeparatorViewDescriptor.swift */, 4B5568B61DD7D846000C033E /* ButtonViewDescriptor.swift */, + A1B9C1BD2AA9E59D00238AF6 /* CameraViewDescriptor.swift */, ); path = ViewDescriptors; sourceTree = ""; @@ -1136,6 +1138,23 @@ path = "Complex-Update-Value"; sourceTree = ""; }; + A182CEDB2B025AE8000385D2 /* Camera */ = { + isa = PBXGroup; + children = ( + A182CEDC2B025B23000385D2 /* CameraView.swift */, + A182CEDE2B025B41000385D2 /* CameraModel.swift */, + A182CEE02B025B53000385D2 /* CameraService.swift */, + A182CEE22B025B6C000385D2 /* CameraInput.swift */, + A182CEE62B02DF10000385D2 /* CameraService+Enums.swift */, + A15891EF2B10BFD000A169C3 /* MetalView.swift */, + A15891F12B10BFED00A169C3 /* MetalRenderer.swift */, + A14B3A462B7AD2960016E380 /* ExperimentCameraInput.swift */, + A18E13572B7CAFF4006EB323 /* ExperimentCameraInputSession.swift */, + A18E13592B7F563B006EB323 /* CameraUIView.swift */, + ); + path = Camera; + sourceTree = ""; + }; A1E03AA129B779A400CE93A6 /* Helper */ = { isa = PBXGroup; children = ( @@ -1194,22 +1213,10 @@ C06BCD262707AADC00B29595 /* ImageIO.framework */, C0FC14882707C19900512064 /* ZipZap.xcodeproj */, C0FC14812707C15200512064 /* JXLSiOS.xcodeproj */, - C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */, ); name = Frameworks; sourceTree = ""; }; - C086C6E3270755E100348772 /* Products */ = { - isa = PBXGroup; - children = ( - C086C6EA270755E100348772 /* CocoaMQTT.framework */, - C086C6EC270755E100348772 /* CocoaMQTT-Tests.xctest */, - C086C6EE270755E100348772 /* CocoaMQTT.framework */, - C086C6F0270755E100348772 /* CocoaMQTT.framework */, - ); - name = Products; - sourceTree = ""; - }; C0FC14822707C15200512064 /* Products */ = { isa = PBXGroup; children = ( @@ -1252,8 +1259,9 @@ name = phyphox; packageProductDependencies = ( C0BD4DF527079C600020A5AF /* JGProgressHUD */, - C0F430C228E5A16D00BC98DE /* GCDWebServers */, C0F430C528E5A2DB00BC98DE /* BEMCheckBox */, + 867E2BD72AC464D000ABB472 /* CocoaMQTT */, + 860E13F22B514703008FAFC6 /* GCDWebServer */, ); productName = phyphox; productReference = 6B7BF77A1C130425007408FB /* phyphox.app */; @@ -1308,7 +1316,6 @@ 6B7BF7791C130425007408FB = { CreatedOnToolsVersion = 7.1.1; LastSwiftMigration = 1230; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; @@ -1359,16 +1366,13 @@ mainGroup = 6B7BF7711C130425007408FB; packageReferences = ( C0BD4DF427079C600020A5AF /* XCRemoteSwiftPackageReference "JGProgressHUD" */, - C0F430C128E5A16D00BC98DE /* XCRemoteSwiftPackageReference "GCDWebServer" */, C0F430C428E5A2DB00BC98DE /* XCRemoteSwiftPackageReference "BEMCheckBox" */, + 867E2BD62AC464D000ABB472 /* XCRemoteSwiftPackageReference "CocoaMQTT" */, + 860E13F12B514703008FAFC6 /* XCRemoteSwiftPackageReference "GCDWebServer" */, ); productRefGroup = 6B7BF77B1C130425007408FB /* Products */; projectDirPath = ""; projectReferences = ( - { - ProductGroup = C086C6E3270755E100348772 /* Products */; - ProjectRef = C086C6E2270755E100348772 /* CocoaMQTT.xcodeproj */; - }, { ProductGroup = C0FC14822707C15200512064 /* Products */; ProjectRef = C0FC14812707C15200512064 /* JXLSiOS.xcodeproj */; @@ -1388,34 +1392,6 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - C086C6EA270755E100348772 /* CocoaMQTT.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CocoaMQTT.framework; - remoteRef = C086C6E9270755E100348772 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C086C6EC270755E100348772 /* CocoaMQTT-Tests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = "CocoaMQTT-Tests.xctest"; - remoteRef = C086C6EB270755E100348772 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C086C6EE270755E100348772 /* CocoaMQTT.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CocoaMQTT.framework; - remoteRef = C086C6ED270755E100348772 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - C086C6F0270755E100348772 /* CocoaMQTT.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = CocoaMQTT.framework; - remoteRef = C086C6EF270755E100348772 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; C0FC14862707C15200512064 /* libJXLS.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1548,6 +1524,7 @@ 6BC011AD1CAA86670053D0DF /* ExperimentTranslationCollection.swift in Sources */, C07B53092294267400BA085A /* ExperimentBluetoothInput.swift in Sources */, 6B63199B1C4969DA0038BE22 /* MainNavigationBar.swift in Sources */, + A15891F02B10BFD000A169C3 /* MetalView.swift in Sources */, 6B63196A1C4849900038BE22 /* CollectionView.swift in Sources */, C07BBC1F26209CAC0097C57A /* ExperimentTimedRunDialogView.swift in Sources */, 6B7BF80F1C147244007408FB /* TanAnalysis.swift in Sources */, @@ -1559,6 +1536,7 @@ C063C662238ECCD000327AE7 /* NetworkDiscovery.swift in Sources */, 6B63C8E41CC14A96007DC09B /* WebServerUtilities.swift in Sources */, 6B0C71501C1C6FFE00500FEE /* GraphViewDescriptor.swift in Sources */, + A18E13582B7CAFF4006EB323 /* ExperimentCameraInputSession.swift in Sources */, 6B6319681C48457B0038BE22 /* CollectionViewController.swift in Sources */, 6BAEC1DE1C1F749000FBD979 /* ValueViewDescriptor.swift in Sources */, 6B06C2161CC770200030BB7D /* ExperimentWebServer.swift in Sources */, @@ -1570,12 +1548,15 @@ 6B7BF7BF1C130471007408FB /* MainView.swift in Sources */, C063C666238ECF7A00327AE7 /* NetworkConversion.swift in Sources */, 6B612FF4207EA4E5001DA8D9 /* OutputElementHandler.swift in Sources */, + A182CEE12B025B53000385D2 /* CameraService.swift in Sources */, 6BCD496720505DA70079E569 /* ExperimentGraphUtilities.swift in Sources */, + A1B9C1BE2AA9E59D00238AF6 /* CameraViewDescriptor.swift in Sources */, 6B612FFD207F70D9001DA8D9 /* ExperimentAnalysisFactory.swift in Sources */, C0A97FE72477DCBF00A630EE /* SortAnalysis.swift in Sources */, 4B47B0371DA6AF2400CCBA6E /* SinhAnalysis.swift in Sources */, C060904E21E28727000BEE30 /* ExperimentPickerViewController.swift in Sources */, 4BE7CAF91E4CD73D00C31090 /* ExperimentSeparatorView.swift in Sources */, + A182CEE32B025B6C000385D2 /* CameraInput.swift in Sources */, A1FA9A0429A7ACE800E0FDBF /* ColorConverterHelper.swift in Sources */, C0914ED922688F1E00000A2F /* FormulaParser.swift in Sources */, 4BE1DEFB1E5096F400F10C71 /* SubrangeAnalysis.swift in Sources */, @@ -1590,6 +1571,7 @@ 6B94A6211CA015A4008D9ACF /* GLGraphView.swift in Sources */, A1E03A7F29B61A9000CE93A6 /* UIImageExt.swift in Sources */, 6B612FF6207EB052001DA8D9 /* AnalysisElementHandler.swift in Sources */, + A14B3A472B7AD2960016E380 /* ExperimentCameraInput.swift in Sources */, 6BAEC1DC1C1F6EFC00FBD979 /* ViewDescriptor.swift in Sources */, 4B47B02D1DA6A1E100CCBA6E /* AcosAnalysis.swift in Sources */, 6B9E3DA0205296E0008073D5 /* ExperimentViewModuleTableViewCell.swift in Sources */, @@ -1601,8 +1583,10 @@ 6BB52BC61C1B11CA00217CF1 /* ExperimentTranslation.swift in Sources */, 6BE320AA1CB2B55700DCC1ED /* TextFieldTableViewCell.swift in Sources */, 6B7BF80D1C14723D007408FB /* CosAnalysis.swift in Sources */, + A18E135A2B7F563B006EB323 /* CameraUIView.swift in Sources */, 6BE320A11CB19E0300DCC1ED /* CreateViewControllerTransition.swift in Sources */, C0A3BE5B2390070900951C23 /* NetworkElementHandler.swift in Sources */, + A1B9C1C02AA9E76C00238AF6 /* CameraViewElementHandler.swift in Sources */, 6BBD28AC1C20BF29009E3757 /* InfoViewDescriptor.swift in Sources */, 4B09C6C51CD78E79004C7FA1 /* MinAnalysis.swift in Sources */, 6B7BF8231C147D09007408FB /* FFTAnalysis.swift in Sources */, @@ -1642,6 +1626,7 @@ 4B47B03B1DA6AF6900CCBA6E /* TanhAnalysis.swift in Sources */, 6B00F0A21C457B5D00971941 /* ExperimentValueView.swift in Sources */, C08725112241460500079279 /* ColorElementHandler.swift in Sources */, + A15891F22B10BFED00A169C3 /* MetalRenderer.swift in Sources */, C05CA3262715D0BD00452FB7 /* ExperimentDepthInput.swift in Sources */, 6B7BF8171C147B8F007408FB /* RampGeneratorAnalysis.swift in Sources */, 6B7BF8211C147CF3007408FB /* AutocorrelationAnalysis.swift in Sources */, @@ -1664,6 +1649,7 @@ C05CD472258C904C00662BDF /* EventsElementHandler.swift in Sources */, 866632E92A29E1D90068762D /* UIColorExtensions.swift in Sources */, 4B5568B51DD7D1F4000C033E /* ExperimentButtonView.swift in Sources */, + A182CEDF2B025B41000385D2 /* CameraModel.swift in Sources */, 6B7BF81B1C147CA8007408FB /* GaussSmoothAnalysis.swift in Sources */, 6B00F0BA1C46F24400971941 /* ExperimentExportSet.swift in Sources */, C01F80FC226A05C5001B0DF8 /* FormulaAnalysis.swift in Sources */, @@ -1673,6 +1659,7 @@ 6BE320B21CB3C0DE00DCC1ED /* PTButton.m in Sources */, 4B47B0331DA6A87A00CCBA6E /* CountAnalysis.swift in Sources */, A1A6A72F297685DF00090309 /* Utility.swift in Sources */, + A182CEE72B02DF10000385D2 /* CameraService+Enums.swift in Sources */, 6B7BF81F1C147CE0007408FB /* DifferentiationAnalysis.swift in Sources */, 6B7BF8051C146B23007408FB /* GCDAnalysis.swift in Sources */, 6BEA9457207FF8CE0077274A /* GraphViewElementHandler.swift in Sources */, @@ -1686,8 +1673,10 @@ C0AE8A982294558000A3EC48 /* ConfigConversion.swift in Sources */, C0A97FF92477E39000A630EE /* InterpolateAnalysis.swift in Sources */, 6B612FE9207E5ABC001DA8D9 /* LinkElementHandler.swift in Sources */, + A101AB0829BB801600B82311 /* ConnectedBluetoothDeviceViewController.swift in Sources */, C05CD336258BA0DF00662BDF /* ExperimentTimeReference.swift in Sources */, 6B7BF7F81C1374A6007408FB /* ExperimentAnalysisModule.swift in Sources */, + A1B9C1C42AA9EC9600238AF6 /* ExperimentCameraGUIView.swift in Sources */, 6B7BF8001C139649007408FB /* DivisionAnalysis.swift in Sources */, C07B530B229426AD00BA085A /* ExperimentBluetoothOutput.swift in Sources */, 6BBA063A1C518BC900E4B1B0 /* UpdateValueAnalysis.swift in Sources */, @@ -1708,6 +1697,7 @@ 6B26F6FD1CAAEDFB00EA9367 /* ExperimentExportSetSelectionView.swift in Sources */, C034B02727172BF500C0EF00 /* Shaders.metal in Sources */, 6B6A4C202056A367007586D0 /* GLGraphShaderProgram.swift in Sources */, + A182CEDD2B025B23000385D2 /* CameraView.swift in Sources */, 6B8AA3F21C5E2A6400E59685 /* ExperimentComplexUpdateValueAnalysis.swift in Sources */, 6B422E2C1CA4A4BF00E945BC /* GraphGridView.swift in Sources */, 4B486EE11EC4B34D00F1045E /* CreditsView.swift in Sources */, @@ -1954,8 +1944,8 @@ CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = NO; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8453; DEVELOPMENT_TEAM = 44GTV4NJA9; ENABLE_BITCODE = NO; @@ -1980,18 +1970,19 @@ GCC_WARN_UNUSED_PARAMETER = NO; HEADER_SEARCH_PATHS = ../ZipZap; INFOPLIST_FILE = phyphox/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ""; - MARKETING_VERSION = 1.1.12; + MARKETING_VERSION = 1.1.13; OTHER_LDFLAGS = ""; "OTHER_SWIFT_FLAGS[arch=*]" = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "de.rwth-aachen.physics.phyphox"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = NO; SWIFT_OBJC_BRIDGING_HEADER = "phyphox/phyphox-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -2020,8 +2011,8 @@ CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = NO; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = NO; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 8453; DEVELOPMENT_TEAM = 44GTV4NJA9; ENABLE_BITCODE = NO; @@ -2041,17 +2032,18 @@ GCC_WARN_UNUSED_PARAMETER = NO; HEADER_SEARCH_PATHS = ../ZipZap; INFOPLIST_FILE = phyphox/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ""; - MARKETING_VERSION = 1.1.12; + MARKETING_VERSION = 1.1.13; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = "de.rwth-aachen.physics.phyphox"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = NO; SWIFT_OBJC_BRIDGING_HEADER = "phyphox/phyphox-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; @@ -2224,20 +2216,28 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - C0BD4DF427079C600020A5AF /* XCRemoteSwiftPackageReference "JGProgressHUD" */ = { + 860E13F12B514703008FAFC6 /* XCRemoteSwiftPackageReference "GCDWebServer" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/JonasGessner/JGProgressHUD"; + repositoryURL = "https://github.com/yene/GCDWebServer"; requirement = { - branch = master; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 3.5.7; }; }; - C0F430C128E5A16D00BC98DE /* XCRemoteSwiftPackageReference "GCDWebServer" */ = { + 867E2BD62AC464D000ABB472 /* XCRemoteSwiftPackageReference "CocoaMQTT" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/JacobHearst/GCDWebServer"; + repositoryURL = "https://github.com/emqx/CocoaMQTT"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 3.0.0; + minimumVersion = 2.0.0; + }; + }; + C0BD4DF427079C600020A5AF /* XCRemoteSwiftPackageReference "JGProgressHUD" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/JonasGessner/JGProgressHUD"; + requirement = { + branch = master; + kind = branch; }; }; C0F430C428E5A2DB00BC98DE /* XCRemoteSwiftPackageReference "BEMCheckBox" */ = { @@ -2251,16 +2251,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 860E13F22B514703008FAFC6 /* GCDWebServer */ = { + isa = XCSwiftPackageProductDependency; + package = 860E13F12B514703008FAFC6 /* XCRemoteSwiftPackageReference "GCDWebServer" */; + productName = GCDWebServer; + }; + 867E2BD72AC464D000ABB472 /* CocoaMQTT */ = { + isa = XCSwiftPackageProductDependency; + package = 867E2BD62AC464D000ABB472 /* XCRemoteSwiftPackageReference "CocoaMQTT" */; + productName = CocoaMQTT; + }; C0BD4DF527079C600020A5AF /* JGProgressHUD */ = { isa = XCSwiftPackageProductDependency; package = C0BD4DF427079C600020A5AF /* XCRemoteSwiftPackageReference "JGProgressHUD" */; productName = JGProgressHUD; }; - C0F430C228E5A16D00BC98DE /* GCDWebServers */ = { - isa = XCSwiftPackageProductDependency; - package = C0F430C128E5A16D00BC98DE /* XCRemoteSwiftPackageReference "GCDWebServer" */; - productName = GCDWebServers; - }; C0F430C528E5A2DB00BC98DE /* BEMCheckBox */ = { isa = XCSwiftPackageProductDependency; package = C0F430C428E5A2DB00BC98DE /* XCRemoteSwiftPackageReference "BEMCheckBox" */; diff --git a/phyphox-iOS/phyphox/AppDelegate.swift b/phyphox-iOS/phyphox/AppDelegate.swift index dd382bcd..3d31cc49 100644 --- a/phyphox-iOS/phyphox/AppDelegate.swift +++ b/phyphox-iOS/phyphox/AppDelegate.swift @@ -13,6 +13,7 @@ let ResignActiveNotification = "ResignActiveNotification" let DidBecomeActiveNotification = "DidBecomeActiveNotification" @UIApplicationMain + final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? diff --git a/phyphox-iOS/phyphox/Assets.xcassets/Image 1.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/Image 1.imageset/Contents.json new file mode 100644 index 00000000..a19a5492 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/Image 1.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/Contents.json new file mode 100644 index 00000000..26191d1d --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cellularbars.level.0.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/cellularbars.level.0.svg b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/cellularbars.level.0.svg new file mode 100644 index 00000000..0329b71a --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_0.imageset/cellularbars.level.0.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/Contents.json new file mode 100644 index 00000000..ff377dac --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cellularbars.level.1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/cellularbars.level.1.svg b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/cellularbars.level.1.svg new file mode 100644 index 00000000..6e423b83 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_1.imageset/cellularbars.level.1.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/Contents.json new file mode 100644 index 00000000..6274f66f --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cellularbars.level.2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/cellularbars.level.2.svg b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/cellularbars.level.2.svg new file mode 100644 index 00000000..60ce3bdb --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_2.imageset/cellularbars.level.2.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/Contents.json new file mode 100644 index 00000000..09e7a299 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cellularbars.level.3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/cellularbars.level.3.svg b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/cellularbars.level.3.svg new file mode 100644 index 00000000..31c73083 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_3.imageset/cellularbars.level.3.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/Contents.json new file mode 100644 index 00000000..8fb9572c --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cellularbars.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/cellularbars.svg b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/cellularbars.svg new file mode 100644 index 00000000..2701cd8f --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/cellular_level_4.imageset/cellularbars.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/Contents.json new file mode 100644 index 00000000..a2b28095 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "ic_flip_camera_android-4 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_flip_camera_android.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_flip_camera_android-4.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4 1.svg new file mode 100644 index 00000000..58202e49 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4.svg b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4.svg new file mode 100644 index 00000000..58202e49 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android.svg b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android.svg new file mode 100644 index 00000000..76ab9374 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/flip_camera.imageset/ic_flip_camera_android.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/Contents.json new file mode 100644 index 00000000..e0cbab24 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "ic_auto_exposure-3 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_auto_exposure.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_auto_exposure-3.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3 1.svg new file mode 100644 index 00000000..0cdd1482 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3.svg new file mode 100644 index 00000000..0cdd1482 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure.svg new file mode 100644 index 00000000..8c1e2c63 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_auto_exposure.imageset/ic_auto_exposure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/Contents.json new file mode 100644 index 00000000..9db96c28 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "ic_camera_iso-2.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_camera_iso.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_camera_iso-2 1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2 1.svg new file mode 100644 index 00000000..d816efe8 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2.svg new file mode 100644 index 00000000..d816efe8 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso.svg new file mode 100644 index 00000000..a7c0737b --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_camera_iso.imageset/ic_camera_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/Contents.json new file mode 100644 index 00000000..9776bd02 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "ic_exposure-2 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_exposure.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_exposure-2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2 1.svg new file mode 100644 index 00000000..2c2bce97 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2.svg new file mode 100644 index 00000000..2c2bce97 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure.svg new file mode 100644 index 00000000..733e1956 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_exposure.imageset/ic_exposure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/Contents.json new file mode 100644 index 00000000..59fa48aa --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "baseline_shutter_speed_24-2 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "baseline_shutter_speed_24.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "baseline_shutter_speed_24-2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2 1.svg new file mode 100644 index 00000000..7159ccaf --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2.svg new file mode 100644 index 00000000..7159ccaf --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24.svg new file mode 100644 index 00000000..fc4c3727 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_shutter_speed.imageset/baseline_shutter_speed_24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/Contents.json new file mode 100644 index 00000000..1b622127 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/Contents.json @@ -0,0 +1,31 @@ +{ + "images" : [ + { + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_white_balance.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "white-balance-2.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/ic_white_balance.png b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/ic_white_balance.png new file mode 100644 index 00000000..6b9343a6 Binary files /dev/null and b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/ic_white_balance.png differ diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/white-balance-2.png b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/white-balance-2.png new file mode 100644 index 00000000..de7162e7 Binary files /dev/null and b/phyphox-iOS/phyphox/Assets.xcassets/ic_white_balance.imageset/white-balance-2.png differ diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/Contents.json b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/Contents.json new file mode 100644 index 00000000..6ecf52bd --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "filename" : "ic_zoom-2 1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "ic_zoom.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_zoom-2.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2 1.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2 1.svg new file mode 100644 index 00000000..29bcd036 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2 1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2.svg new file mode 100644 index 00000000..29bcd036 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom.svg b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom.svg new file mode 100644 index 00000000..c4fcf5e7 --- /dev/null +++ b/phyphox-iOS/phyphox/Assets.xcassets/ic_zoom.imageset/ic_zoom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/phyphox-iOS/phyphox/Camera/CameraInput.swift b/phyphox-iOS/phyphox/Camera/CameraInput.swift new file mode 100644 index 00000000..6eafdcb1 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraInput.swift @@ -0,0 +1,11 @@ +// +// CameraInput.swift +// phyphox +// +// Created by Gaurav Tripathee on 13.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation + + diff --git a/phyphox-iOS/phyphox/Camera/CameraModel.swift b/phyphox-iOS/phyphox/Camera/CameraModel.swift new file mode 100644 index 00000000..ac88d5c5 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraModel.swift @@ -0,0 +1,275 @@ +// +// CameraModel.swift +// phyphox +// +// Created by Gaurav Tripathee on 13.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation +import AVFoundation +import MetalKit + + +protocol CameraSelectionDelegate { + var x1: Float { get set } + var x2: Float { get set } + var y1: Float { get set } + var y2: Float { get set } + var timeReference: ExperimentTimeReference? { get set } + var zBuffer: DataBuffer? { get set } + var tBuffer: DataBuffer? { get set } + var exposureSettingLevel: Int { get set } +} + +@available(iOS 14.0, *) +protocol CameraViewDelegate: AnyObject { + var metalView: CameraMetalView { get set } + var metalRenderer: MetalRenderer { get set } + var cameraSettingsModel : CameraSettingsModel { get set } + var isOverlayEditable: Bool { get set } +} + + +protocol CameraSettingDelegate { + var exposureSettingLevel: Int { get set } + var locked: String { get set } +} + + +@available(iOS 14.0, *) +class CameraSettingsModel: ObservableObject, CameraSettingDelegate { + + var cameraSettingLevel: CameraSettingLevel = .ADVANCE + + var cameraSettingMode: CameraSettingMode = .NONE + + var exposureSettingLevel: Int = 0 + var locked: String = "" + + @Published var maxOpticalZoom: Int = 1 + var ultraWideCamera: Bool = false + @Published var minZoom: Int = 1 + @Published var maxZoom: Int = 1 + var currentZoom: Int = 1 + + var minShutterSpeed: Double = 1.0 // min is more less + var maxShutterSpeed: Double = 1.0 // max is near or less more than 1.0 + @Published var currentShutterSpeed: CMTime? + + var minIso: Float = 30.0 + var maxIso: Float = 100.0 + @Published var currentIso: Int = 30 + + var apertureValue: Float = 1.0 + @Published var currentApertureValue: Float = 1.0 + + var minExposureValue: Float = 0.0 + var maxExposureValue: Float = 1.0 + @Published var currentExposureValue: Float = 0.0 + + var autoAxposureEnable: Bool = true + + var minwhiteBalance: Float = 1.0 + var maxWhiteBalance: Float = 1.0 + @Published var currentWhiteBalance: Float = 1.0 + + private var zoomScale: Float = 1.0 + + var exposureCompensationRange: ClosedRange? + + var service: CameraService? + + @available(iOS 14.0, *) + init(service: CameraService){ + self.service = service + } + + init(){} + + + func getScale() -> CGFloat { + service?.zoomScale ?? 1.0 + + } + + func setScale(scale: CGFloat) { + // service.zoomScale = scale + service?.updateZoom(scale: scale) + } + + func setZoomScale(scale: Float) { + zoomScale = scale + } + + func getZoomScale() -> Float { + return zoomScale + } + + func switchCamera(){ + service?.changeCamera() + } + + func getLisOfCameraSettingsValue(cameraSettingMode: CameraSettingMode) -> [Float] { + service?.getSelectableValuesForCameraSettings(cameraSettingMode: cameraSettingMode) ?? [] + } + + + func autoExposure(auto: Bool) { + service?.setExposureTo(auto: auto) + } + + func exposure(value: Float) { + service?.changeExposure(value: value) + } + + func aperture() {} + + func iso(value: Int) { + service?.changeISO(value) + } + + func shutterSpeed(value: Double) { + service?.changeExposureDuration(value) + } + + func zoom() {} + + func whiteBalance() {} + + var defaultCameraSetting: AVCaptureDevice? { + + // Find the built-in Dual Camera, if it exists. + if let device = AVCaptureDevice.default(.builtInTripleCamera, + for: .video, + position: .back) { + return device + } + + // Find the built-in Dual Wide Camera, if it exists. (consist of wide and ultra wide camera) + if let device = AVCaptureDevice.default(.builtInDualWideCamera, + for: .video, + position: .back) { + return device + } + + // Find the built-in Wide-Angle Camera, if it exists. + if let device = AVCaptureDevice.default(.builtInWideAngleCamera, + for: .video, + position: .back) { + return device + } + + return nil + } + +} + + +@available(iOS 14.0, *) +final class CameraModel: ObservableObject, CameraViewDelegate, CameraSelectionDelegate{ + + var x1: Float = 0.4 + var x2: Float = 0.6 + var y1: Float = 0.4 + var y2: Float = 0.6 + + var exposureSettingLevel: Int = 3 + + private let service = CameraService() + var metalView = CameraMetalView() + var cameraSettingsModel : CameraSettingsModel + + var metalRenderer: MetalRenderer + var session: AVCaptureSession + + var timeReference: ExperimentTimeReference? + var zBuffer: DataBuffer? + var tBuffer: DataBuffer? + + var isOverlayEditable: Bool = false + + /// The app's default camera. + var defaultCamera: AVCaptureDevice? { + + // Find the built-in Dual Camera, if it exists. + if let device = AVCaptureDevice.default(.builtInTripleCamera, + for: .video, + position: .back) { + return device + } + + // Find the built-in Dual Wide Camera, if it exists. (consist of wide and ultra wide camera) + if let device = AVCaptureDevice.default(.builtInDualWideCamera, + for: .video, + position: .back) { + return device + } + + + return AVCaptureDevice.default(.builtInWideAngleCamera, + for: .video, + position: .back) + } + + + init() { + self.session = service.session + self.metalRenderer = MetalRenderer(parent: metalView, renderer: metalView.metalView) + + cameraSettingsModel = CameraSettingsModel(service: service) + + configure() + + initModel(model: self) + } + + + + func configure(){ + service.checkForPermisssion() + service.configure() + + metalView.metalView.delegate = metalRenderer + service.metalRender = metalRenderer + } + + func initModel(model: CameraModel){ + service.initializeModel(model: model) + } + + + func startSession(){ + service.metalRender?.measuring = true + } + + + func stopSession(){ + service.metalRender?.measuring = false + } + + func endSession(){ + service.session.stopRunning() + } +} + + +enum CameraSettingMode { + case NONE + case ZOOM + case EXPOSURE + case AUTO_EXPOSURE + case SWITCH_LENS + case ISO + case SHUTTER_SPEED + case WHITE_BAlANCE + +} + + +enum CameraSettingLevel { + case BASIC // auto exposure ON (Level 1) + case INTERMEDIATE // auto exposure OFF, only adjust exposure (Level 2) + case ADVANCE // auto exposure OFF, can adjust ISO, Shutter Speed and Aperture (Level 3) +} + diff --git a/phyphox-iOS/phyphox/Camera/CameraService+Enums.swift b/phyphox-iOS/phyphox/Camera/CameraService+Enums.swift new file mode 100644 index 00000000..02faca3f --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraService+Enums.swift @@ -0,0 +1,19 @@ +// +// CameraService+Enums.swift +// phyphox +// +// Created by Gaurav Tripathee on 13.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation + +@available(iOS 14.0, *) +extension CameraService{ + + enum SessionSetupResult { + case success + case notAuthorized + case configurationFailed + } +} diff --git a/phyphox-iOS/phyphox/Camera/CameraService.swift b/phyphox-iOS/phyphox/Camera/CameraService.swift new file mode 100644 index 00000000..0e813c13 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraService.swift @@ -0,0 +1,605 @@ +// +// CameraService.swift +// phyphox +// +// Created by Gaurav Tripathee on 13.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// +// + + +import AVFoundation +import MetalKit +import CoreMedia + +@available(iOS 14.0, *) +public class CameraService: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { + + public let session = AVCaptureSession() + + private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], autoreleaseFrequency: .workItem) + + @objc dynamic var videoDeviceInput: AVCaptureDeviceInput! + + var setupResult: SessionSetupResult = .success + + public var alertError: AlertError = AlertError() + + var isSessionRunning = false + + var isConfigured = false + + var configLocked = false + + @Published public var shouldShowAlertView = false + + @Published public var isCameraUnavailable = true + + private var queue = DispatchQueue(label: "video output queue") + + var metalRender : MetalRenderer? + + var defaultVideoDevice: AVCaptureDevice? + + var cameraModel: CameraModel? + + var cameraSetting: CameraSettingsModel = CameraSettingsModel() + + @Published var zoomScale: CGFloat = 1.0 + + + let defaultMinExposureCMTime = CMTime(value: 14, timescale: 1000000, flags: CMTimeFlags(rawValue: 1), epoch: 0) + let defaultMaxExposureCMTime = CMTime(value: 1, timescale: 1, flags: CMTimeFlags(rawValue: 1), epoch: 0) + + private let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified) + + + public func checkForPermisssion(){ + + switch AVCaptureDevice.authorizationStatus(for: .video) { + + case .authorized: + + break + case .notDetermined: + + sessionQueue.suspend() + AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in + if !granted { + self.setupResult = .notAuthorized + } + self.sessionQueue.resume() + }) + default: + + setupResult = .notAuthorized + + DispatchQueue.main.async { + self.alertError = AlertError(title: "Camera Access", message: "Phyphox doesn't have access to use your camera, please update your privacy settings.", primaryButtonTitle: "Settings", secondaryButtonTitle: nil, primaryAction: { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, + options: [:], completionHandler: nil) + + }, secondaryAction: nil) + self.shouldShowAlertView = true + self.isCameraUnavailable = true + } + + + } + } + + public func configure(){ + + sessionQueue.async { + self.configureSession() + } + + } + + @available(iOS 14.0, *) + func initializeModel(model: CameraModel){ + cameraModel = model + } + + private func changeExposureMode(mode: AVCaptureDevice.ExposureMode){ + lockConfig { () -> () in + if ((self.defaultVideoDevice?.isExposureModeSupported(mode)) != nil) { + self.defaultVideoDevice?.exposureMode = mode + } + } + + } + // optical zoom range, normal zoom range. + // iphone 12 mini: Dual 12MP, wide and ultra wide, wide : f/1,6, ultra wide: f/2.4 120 degree field of view, 2x optical zoom out , digital zoom upto 5x, + + func updateZoom(scale: CGFloat){ + lockConfig { () -> () in + defaultVideoDevice?.videoZoomFactor = max(1.0, min(zoomScale, (defaultVideoDevice?.activeFormat.videoMaxZoomFactor) ?? 1.0)) + zoomScale = scale + } + } + + + func lockConfig(complete: () -> ()) { + if isConfigured { + configLocked = true + do{ + try defaultVideoDevice?.lockForConfiguration() + complete() + defaultVideoDevice?.unlockForConfiguration() + configLocked = false + } catch { + configLocked = false + + } + } + } + + func getAvailableOpticalZoomList(maxOpticalZoom_: Int?) -> [Int] { + guard let maxOpticalZoom = maxOpticalZoom_ else { + return [] + } + + if maxOpticalZoom == 1 { + return [] + } + + if maxOpticalZoom < 5 { + return [1,2] + } + + if maxOpticalZoom < 10 { + return [1,2,5] + } + + if maxOpticalZoom < 15 { + return [1,2,5,10] + } + + return [] + } + + func shutterSpeedRange(min: Int, max: Int) -> [Int] { + let filteredShutterSpeedRange = shutters.filter{$0 >= min && $0 <= max} + + return filteredShutterSpeedRange + } + + func getShutterSpeedRange() -> [Int] { + var shutters_available: [Float] = [] + + let min_seconds = CMTimeGetSeconds((cameraModel?.defaultCamera?.activeFormat.minExposureDuration) ?? defaultMinExposureCMTime) + let max_seconds = CMTimeGetSeconds((cameraModel?.defaultCamera?.activeFormat.maxExposureDuration) ?? defaultMinExposureCMTime) + + for one_shutter in shutters { + let seconds = 1.0 / Float64(one_shutter) + if seconds >= min_seconds && seconds <= max_seconds { + shutters_available.append(Float(one_shutter)) + } + } + + return shutters_available.map { Int($0) } + + } + + func getNearestValue(value: Int, numbers: [Int]) -> Int{ + + if(numbers.contains(value)){ + return value + } + + for (index, number) in numbers.enumerated() { + if(number > value){ + let average = (number + numbers[index - 1])/2 + if(value > average){ + return number + } else { + return numbers[index - 1] + } + } + } + return value + } + + + func isoRange(min: Int, max: Int) -> [Int] { + let filteredIsoRange = iso.filter { $0 >= min && $0 <= max } + + return filteredIsoRange + } + + func findIsoNearestNumber(input: Int, numbers: [Int]) -> Int { + var nearestNumber = numbers[0] + var difference = abs(input - nearestNumber) + + for number in numbers { + let currentDifference = abs(input - number) + if currentDifference < difference { + difference = currentDifference + nearestNumber = number + } + } + + return nearestNumber + } + + func getSelectableValuesForCameraSettings(cameraSettingMode: CameraSettingMode) -> [Float] { + switch cameraSettingMode { + case .ZOOM: + var zoomList : [Float] = [] + if(cameraModel?.cameraSettingsModel.ultraWideCamera == true){ + zoomList.append(0.5) + } + + if(cameraModel?.cameraSettingsModel.maxOpticalZoom != nil ){ + let zoomValue = getAvailableOpticalZoomList(maxOpticalZoom_: cameraModel?.cameraSettingsModel.maxOpticalZoom) + for value in zoomValue{ + zoomList.append(Float(value)) + } + //zoomList.append(Float((cameraModel?.cameraSettingsModel.maxOpticalZoom)! * 3 )) + } + return zoomList.map{Float($0)} + + case .EXPOSURE: + return getExposureValues() + case .AUTO_EXPOSURE: + return [] + case .SWITCH_LENS: + return [] + case .ISO: + return isoRange(min: Int((cameraModel?.cameraSettingsModel.minIso) ?? 30.0), + max: Int((cameraModel?.cameraSettingsModel.maxIso) ?? 100.0)).map{Float($0)} + case .SHUTTER_SPEED: + return getShutterSpeedRange().map{Float($0)} + case .WHITE_BAlANCE: + return [] + case .NONE: + return [] + } + } + + + func getCameraSettingsInfo(){ + + cameraModel?.cameraSettingsModel.minIso = cameraModel?.defaultCamera?.activeFormat.minISO ?? 30.0 + cameraModel?.cameraSettingsModel.maxIso = cameraModel?.defaultCamera?.activeFormat.maxISO ?? 100.0 + + + cameraModel?.cameraSettingsModel.minShutterSpeed = CMTimeGetSeconds((cameraModel?.defaultCamera?.activeFormat.minExposureDuration) ?? defaultMinExposureCMTime) + cameraModel?.cameraSettingsModel.maxShutterSpeed = CMTimeGetSeconds((cameraModel?.defaultCamera?.activeFormat.maxExposureDuration) ?? defaultMaxExposureCMTime) + + cameraModel?.cameraSettingsModel.apertureValue = (cameraModel?.defaultCamera?.lensAperture) ?? 1.0 + + //cameraModel?.cameraSettingsModel.minZoom = (cameraModel?.defaultCamera?.minAvailableVideoZoomFactor.rounded().hashValue)! + //cameraModel?.cameraSettingsModel.maxZoom = (cameraModel?.defaultCamera?.maxAvailableVideoZoomFactor.rounded().hashValue)! + + cameraModel?.cameraSettingsModel.maxOpticalZoom = cameraModel?.defaultCamera?.virtualDeviceSwitchOverVideoZoomFactors.last?.intValue ?? 1 + + if(cameraModel?.defaultCamera?.deviceType == AVCaptureDevice.DeviceType.builtInDualWideCamera || + cameraModel?.defaultCamera?.deviceType == AVCaptureDevice.DeviceType.builtInTripleCamera){ + cameraModel?.cameraSettingsModel.ultraWideCamera = true + } + } + + func setCameraSettinginfo(){ + self.cameraModel?.cameraSettingsModel.currentApertureValue = self.defaultVideoDevice?.lensAperture ?? 1.0 + self.cameraModel?.cameraSettingsModel.currentIso = self.getNearestValue(value: Int(self.defaultVideoDevice?.iso ?? 30.0), numbers: iso) + + self.cameraModel?.cameraSettingsModel.currentShutterSpeed = (self.defaultVideoDevice?.exposureDuration) + + self.cameraModel?.cameraSettingsModel.minZoom = Int((self.defaultVideoDevice?.minAvailableVideoZoomFactor ?? 1.0)) + + if(self.defaultVideoDevice?.virtualDeviceSwitchOverVideoZoomFactors.isEmpty == true){ + self.cameraModel?.cameraSettingsModel.maxZoom = Int((self.defaultVideoDevice?.maxAvailableVideoZoomFactor ?? 1.0) ) / 10 + } else { + self.cameraModel?.cameraSettingsModel.maxZoom = (self.defaultVideoDevice?.virtualDeviceSwitchOverVideoZoomFactors.last?.intValue ?? 1) * 3 + } + + self.cameraModel?.cameraSettingsModel.maxOpticalZoom = self.defaultVideoDevice?.virtualDeviceSwitchOverVideoZoomFactors.last?.intValue ?? 1 + + let minExposure = self.defaultVideoDevice?.minExposureTargetBias + let maxExposure = self.defaultVideoDevice?.maxExposureTargetBias + + self.cameraModel?.cameraSettingsModel.exposureCompensationRange = (minExposure ?? -8.0)...(maxExposure ?? 8.0) + + + } + + func changeISO(_ iso: Int) { + + let duration_seconds = (cameraModel?.cameraSettingsModel.currentShutterSpeed) ?? defaultMinExposureCMTime + + if (defaultVideoDevice?.isExposureModeSupported(.locked) == true){ + lockConfig { () -> () in + defaultVideoDevice?.exposureMode = .custom + defaultVideoDevice?.setExposureModeCustom(duration: duration_seconds , iso: Float(iso), completionHandler: nil) + cameraModel?.cameraSettingsModel.currentIso = iso + + } + } else { + print("custom exposure setting not supported") + } + + } + + func changeExposureDuration(_ p: Double) { + let seconds = 1.0 / Float64(p) + let duration_seconds = CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000*1000*1000 ) + if (defaultVideoDevice?.isExposureModeSupported(.locked) == true){ + lockConfig { () -> () in + defaultVideoDevice?.exposureMode = .custom + defaultVideoDevice?.setExposureModeCustom(duration: duration_seconds , iso: AVCaptureDevice.currentISO, completionHandler: nil) + cameraModel?.cameraSettingsModel.currentShutterSpeed = duration_seconds + } + } else { + print("custom exposure setting not supported") + } + + } + + func changeExpoDuration(){ + if (defaultVideoDevice?.isExposureModeSupported(.locked) == true){ + do { + try defaultVideoDevice?.lockForConfiguration() + defaultVideoDevice?.exposureMode = .custom + defaultVideoDevice?.unlockForConfiguration() + } catch { + print("Error setting camera shutter speed: \(error.localizedDescription)") + } + } + else { + print("custom exposure setting not supported") + } + } + + func setExposureTo(auto: Bool){ + do { + try defaultVideoDevice?.lockForConfiguration() + defaultVideoDevice?.exposureMode = auto ? .autoExpose : .custom + defaultVideoDevice?.unlockForConfiguration() + } catch { + print("Error setting camera exposure: \(error.localizedDescription)") + } + print("is already in autoexposure") + } + + func changeExposure(value: Float) { + + if (defaultVideoDevice?.isExposureModeSupported(.locked) == true && defaultVideoDevice?.isExposureModeSupported(.continuousAutoExposure) == true){ + do { + try defaultVideoDevice?.lockForConfiguration() + defaultVideoDevice?.exposureMode = .continuousAutoExposure + defaultVideoDevice!.setExposureTargetBias(value, completionHandler: nil) + defaultVideoDevice?.unlockForConfiguration() + } catch { + print("Error setting camera expsoure: \(error.localizedDescription)") + } + } + else { + print("custom exposure setting not supported") + } + } + + func getExposureValues() -> [Float] { + return getExposureValuesFromRange(min: Int(cameraModel?.cameraSettingsModel.exposureCompensationRange?.lowerBound ?? -8.0), + max: Int(cameraModel?.cameraSettingsModel.exposureCompensationRange?.upperBound ?? 8.0), + step: 1) + } + + func getExposureValuesFromRange(min: Int, max: Int, step: Float) -> [Float] { + var exposureValues = [Float]() + + for value in min...max { + let exposureCompensation = Float(value) * step + let decimalPlaces = 1 + let powerOf10 = pow(10.0, Float(decimalPlaces)) + let roundedNumber = round(exposureCompensation * powerOf10) / powerOf10 + exposureValues.append(roundedNumber) + } + + return exposureValues.filter { (Int($0 * 10) % 5) == 0 } + } + + + private func configureSession(){ + + if setupResult != .success { + return + } + + session.beginConfiguration() + session.sessionPreset = .medium + + getCameraSettingsInfo() + + + do { + // builtInDualWideCamera -m virtualDeviceSwitchOverVideoZoomFactors [2] + let defaultCameraType = cameraModel?.defaultCamera?.deviceType ?? AVCaptureDevice.DeviceType.builtInWideAngleCamera + if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { + // If a rear dual camera is not available, default to the rear wide angle camera. + defaultVideoDevice = backCameraDevice + } else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) { + // If the rear wide angle camera isn't available, default to the front wide angle camera. + defaultVideoDevice = frontCameraDevice + } + + setCameraSettinginfo() + + guard let videoDevice = defaultVideoDevice else { + print("Default video device is unavailable.") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + do { + try defaultVideoDevice?.lockForConfiguration() + defaultVideoDevice?.videoZoomFactor = max(1.0, min(zoomScale, (defaultVideoDevice?.activeFormat.videoMaxZoomFactor ?? 1.0))) + defaultVideoDevice?.unlockForConfiguration() + } catch { + print("Error setting camera zoom: \(error.localizedDescription)") + } + + let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) + + if session.canAddInput(videoDeviceInput) { + session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + + } else { + print("Couldn't add video device input to the session.") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + // setup output, add output to our capture session + let captureOutput = AVCaptureVideoDataOutput() + + captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)] + + captureOutput.alwaysDiscardsLateVideoFrames = true + + let captureSessionQueue = DispatchQueue(label: "CameraSessionQueue", attributes: []) + captureOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + + if session.canAddOutput(captureOutput) { + session.addOutput(captureOutput) + } else { + print("Error: Cannot add the Output to the session") + } + + + } catch { + print("Couldn't create video device input: \(error)") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + session.commitConfiguration() + + + self.isConfigured = true + + self.start() + + } + + public func start() { +// We use our capture session queue to ensure our UI runs smoothly on the main thread. + sessionQueue.async { + if !self.isSessionRunning && self.isConfigured { + switch self.setupResult { + case .success: + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + + if self.session.isRunning { + DispatchQueue.main.async { + self.isCameraUnavailable = false + } + } + + case .configurationFailed, .notAuthorized: + print("Application not authorized to use camera") + + DispatchQueue.main.async { + self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or its missing permissions", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil) + self.shouldShowAlertView = true + self.isCameraUnavailable = true + } + } + } + } + } + + /// - Tag: ChangeCamera + public func changeCamera() { + + sessionQueue.async { + let currentVideoDevice = self.videoDeviceInput.device + let currentPosition = currentVideoDevice.position + + let preferredPosition: AVCaptureDevice.Position + let preferredDeviceType: AVCaptureDevice.DeviceType + + switch currentPosition { + case .unspecified, .front: + preferredPosition = .back + preferredDeviceType = .builtInWideAngleCamera + + case .back: + preferredPosition = .front + preferredDeviceType = .builtInWideAngleCamera + + @unknown default: + print("Unknown capture position. Defaulting to back, dual-camera.") + preferredPosition = .back + preferredDeviceType = .builtInWideAngleCamera + } + let devices = self.videoDeviceDiscoverySession.devices + var newVideoDevice: AVCaptureDevice? = nil + + // First, seek a device with both the preferred position and device type. Otherwise, seek a device with only the preferred position. + if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) { + newVideoDevice = device + } else if let device = devices.first(where: { $0.position == preferredPosition }) { + newVideoDevice = device + } + + if let videoDevice = newVideoDevice { + do { + let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) + + self.session.beginConfiguration() + + // Remove the existing device input first, because AVCaptureSession doesn't support + // simultaneous use of the rear and front cameras. + self.session.removeInput(self.videoDeviceInput) + + if self.session.canAddInput(videoDeviceInput) { + self.session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + } else { + self.session.addInput(self.videoDeviceInput) + } + self.defaultVideoDevice = videoDevice + + self.setCameraSettinginfo() + + self.session.commitConfiguration() + } catch { + print("Error occurred while creating video device input: \(error)") + } + + } + } + } + + public func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + print("captureOutput didDrop") + } + + + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + + let presentationTimestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + let seconds = CMTimeGetSeconds(presentationTimestamp) + + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + let width = CVPixelBufferGetWidth(imageBuffer) + let height = CVPixelBufferGetHeight(imageBuffer) + + print("Image Resolution: \(width)x\(height)") + + self.metalRender?.updateFrame(imageBuffer: imageBuffer, selectionState: MetalRenderer.SelectionStruct( + x1: cameraModel?.x1 ?? 0, x2: cameraModel?.x2 ?? 0, y1: cameraModel?.y1 ?? 0, y2: cameraModel?.y2 ?? 0, editable: cameraModel?.isOverlayEditable ?? true), time: seconds) + + } + +} diff --git a/phyphox-iOS/phyphox/Camera/CameraUIView.swift b/phyphox-iOS/phyphox/Camera/CameraUIView.swift new file mode 100644 index 00000000..315b55e8 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraUIView.swift @@ -0,0 +1,144 @@ +// +// CameraUIView.swift +// phyphox +// +// Created by Gaurav Tripathee on 16.02.24. +// Copyright © 2024 RWTH Aachen. All rights reserved. +// + +import Foundation +import AVFoundation +import SwiftUI +import MetalKit +import Combine + +private enum CameraGestureState { + case begin + case end + case none +} + +@available(iOS 13.0, *) +class CameraUIDataModel: ObservableObject { + @Published var cameraIsMaximized: Bool = false +} + + +@available(iOS 14.0, *) +final class ExperimentCameraUIView: UIView, CameraGUIDelegate { + + func updateFrame(captureSession: AVCaptureSession) { + print("update frame") + } + + func updateResolution(resolution: CGSize) { + setNeedsLayout() + } + + + var cameraSelectionDelegate: CameraSelectionDelegate? + var cameraViewDelegete: CameraViewDelegate? + + let descriptor: CameraViewDescriptor + + let screenWidth = UIScreen.main.bounds.width + let screenHeight = UIScreen.main.bounds.height / 2 + + var resizableState: ResizableViewModuleState = .normal + + let dataModel = CameraUIDataModel() + + private var cancellables = Set() + + required init?(descriptor: CameraViewDescriptor) { + self.descriptor = descriptor + + super.init(frame: .zero) + + + + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + + return size + } + + + override func layoutSubviews() { + super.layoutSubviews() + + let cameraViewModel = CameraViewModel(cameraUIDataModel: dataModel) + + let cameraViewHostingController = UIHostingController(rootView: PhyphoxCameraView( + viewModel: cameraViewModel, cameraSelectionDelegate: cameraSelectionDelegate, cameraViewDelegete: cameraViewDelegete + )) + + let hostingController = UIHostingController(rootView: CameraSettingView( + cameraSettingModel: cameraViewDelegete?.cameraSettingsModel ?? CameraSettingsModel(), + exposureSettingLevel: cameraSelectionDelegate?.exposureSettingLevel ?? 0 + )) + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + cameraViewHostingController.view.translatesAutoresizingMaskIntoConstraints = false + + addSubview(cameraViewHostingController.view) + addSubview(hostingController.view) + + if(dataModel.cameraIsMaximized){ + hostingController.view.isHidden = false + } else { + hostingController.view.isHidden = true + } + + // UI Showing the + dataModel.objectWillChange.sink{ + [weak self] _ in + if(self?.dataModel.cameraIsMaximized == true){ + hostingController.view.isHidden = true + self?.resizableState = .normal + cameraViewHostingController.view.sizeThatFits(CGSize.init(width: self?.screenWidth ?? 400 , height: 250 )) + self?.setNeedsDisplay() + + } else { + hostingController.view.isHidden = false + self?.resizableState = .exclusive + cameraViewHostingController.view.sizeThatFits(CGSize.init(width: self?.screenWidth ?? 400, height: 860)) + self?.setNeedsDisplay() + + + } + }.store(in: &cancellables) + + var constraints = [NSLayoutConstraint]() + + constraints.append(cameraViewHostingController.view.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor)) + constraints.append(cameraViewHostingController.view.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)) + constraints.append(cameraViewHostingController.view.bottomAnchor.constraint(equalTo: hostingController.view.topAnchor)) + constraints.append(cameraViewHostingController.view.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor)) + + + constraints.append(hostingController.view.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor)) + constraints.append(hostingController.view.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor)) + constraints.append(hostingController.view.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)) + constraints.append(hostingController.view.topAnchor.constraint(equalTo: cameraViewHostingController.view.bottomAnchor)) + + constraints.append(hostingController.view.widthAnchor.constraint(equalTo: cameraViewHostingController.view.widthAnchor, multiplier: 1)) + + constraints.append(cameraViewHostingController.view.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.75)) + + constraints.append(cameraViewHostingController.view.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1)) + + constraints.append(hostingController.view.heightAnchor.constraint(equalTo: cameraViewHostingController.view.heightAnchor, multiplier: 0.30)) + + + NSLayoutConstraint.activate(constraints) + + } + +} diff --git a/phyphox-iOS/phyphox/Camera/CameraView.swift b/phyphox-iOS/phyphox/Camera/CameraView.swift new file mode 100644 index 00000000..f0285fcc --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/CameraView.swift @@ -0,0 +1,524 @@ +// +// CameraView.swift +// phyphox +// +// Created by Gaurav Tripathee on 13.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation +import SwiftUI +import Combine +import CoreMedia + +@available(iOS 13.0, *) +class CameraViewModel: ObservableObject { + @Published var cameraUIDataModel: CameraUIDataModel + + init(cameraUIDataModel: CameraUIDataModel) { + self.cameraUIDataModel = cameraUIDataModel + } +} + + +@available(iOS 14.0, *) +struct PhyphoxCameraView: View { + + @ObservedObject var viewModel: CameraViewModel + + var cameraSelectionDelegate: CameraSelectionDelegate? + var cameraViewDelegete: CameraViewDelegate? + + @State private var panningIndexX: Int = 0 + @State private var panningIndexY: Int = 0 + + @State private var modelGesture: CameraGestureState = .none + + @State var isMaximized = false + + @State private var overlayWidth: CGFloat = 50 + @State private var overlayHeight: CGFloat = 50 + + @State private var scale: CGFloat = 1.0 + @State private var currentPosition: CGSize = .zero + @State private var newPosition: CGSize = .zero + + @State private var height: CGFloat = 200.0 + @State private var width: CGFloat = 200.0 + @State private var startPosition: CGFloat = 0.0 + + @State private var speed = 50.0 + + @State private var viewState = CGPoint.zero + + var resizableState: ResizableViewModuleState = .normal + + var mimimizeCameraButton: some View { + Button(action: { + self.isMaximized.toggle() + viewModel.cameraUIDataModel.cameraIsMaximized.toggle() + self.cameraViewDelegete?.isOverlayEditable.toggle() + }, label: { + Image(systemName: isMaximized ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right") + .font(.title2) + .padding() + }) + } + + + var body: some View { + + let dragGesture = DragGesture() + .onChanged{ value in + if(isMaximized){ + viewState = value.location + pannned(locationY: viewState.y, locationX: viewState.x , state: CameraGestureState.begin) + } + + } + .onEnded{ value in + if(isMaximized){ + pannned(locationY: viewState.y, locationX: viewState.x , state: CameraGestureState.end) + self.viewState = .zero + } + } + + + GeometryReader { reader in + ZStack { + + VStack(alignment: .center, spacing: 5) { + + HStack(spacing: 0){ + mimimizeCameraButton + .frame(maxWidth: .infinity, alignment: .leading) + + Text("Preview") + .foregroundColor(Color("textColor")) + .frame(maxWidth: .infinity, alignment: .leading) + + Circle().frame(width: 50, height: 50).opacity(0.0) + + } + + ZStack{ + + + cameraViewDelegete?.metalView + .gesture(dragGesture) + .frame(width: !isMaximized ? reader.size.width / 2 : reader.size.width / 1.2 , + height: !isMaximized ? reader.size.height / 2.5 : reader.size.height / 1.2, + alignment: .topTrailing) + + + } + } + + }//.background(Color.black) + } + } + + + func pannned (locationY: CGFloat, locationX: CGFloat , state: CameraGestureState) { + + guard var del = cameraSelectionDelegate else { + print("camera selection delegate is not accessable") + return + } + + guard let viewDelegate = cameraViewDelegete else { + print("camera view delegate is not accessable") + return + } + + + + let pr = CGPoint(x: locationX / (viewDelegate.metalView.metalView.frame.width ), y: locationY / (viewDelegate.metalView.metalView.frame.height )) + let ps = pr.applying(viewDelegate.metalRenderer.displayToCameraTransform) + let x = Float(ps.x) + let y = Float(ps.y) + + if state == .begin { + let x1Square = (x - del.x1) * (x - del.x1) + let x2Square = (x - del.x2) * (x - del.x2) + let y1Square = (y - del.y1) * (y - del.y1) + let y2Square = (y - del.y2) * (y - del.y2) + + let d11 = x1Square + y1Square + let d12 = x1Square + y2Square + let d21 = x2Square + y1Square + let d22 = x2Square + y2Square + + let _:Float = 0.1 // it was 0.01 for depth, after removing it from if else, it worked. Need to come again for this + if d11 < d12 && d11 < d21 && d11 < d22 { + panningIndexX = 1 + panningIndexY = 1 + } else if d12 < d21 && d12 < d22 { + panningIndexX = 1 + panningIndexY = 2 + } else if d21 < d22 { + panningIndexX = 2 + panningIndexY = 1 + } else { + panningIndexX = 2 + panningIndexY = 2 + } + + if panningIndexX == 1 { + del.x1 = x + } else if panningIndexX == 2 { + del.x2 = x + } + if panningIndexY == 1 { + del.y1 = y + } else if panningIndexY == 2 { + del.y2 = y + } + + } else if state == .end { + if panningIndexX == 1 { + del.x1 = x + } else if panningIndexX == 2 { + del.x2 = x + } + if panningIndexY == 1 { + del.y1 = y + } else if panningIndexY == 2 { + del.y2 = y + } + + } else { + + } + + + } + + enum CameraGestureState { + case begin + case end + case none + } + +} + + +@available(iOS 14.0, *) +struct CameraSettingButton: View { + let action: () -> Void + let image: String + let size: CGFloat + + + var body: some View { + + Button(action: { + action() + }){ + Circle() + .foregroundColor(Color.gray.opacity(0.0)) + .frame(width: 45, height: 45, alignment: .center) + .overlay( + Image(image) + .resizable() + .scaledToFit() + .frame(width: size, height: size) + ) + } + + } +} + +@available(iOS 14.0, *) +struct CameraSettingView: View { + @ObservedObject var cameraSettingModel = CameraSettingsModel() + var exposureSettingLevel: Int + + @State private var cameraSettingMode: CameraSettingMode = .NONE + + @State private var isEditing = false + @State private var isZooming = false + + @State private var rotation: Double = 0.0 + @State private var isFlipped: Bool = false + @State private var autoExposureOff : Bool = true + + @State private var zoomClicked: Bool = false + + @State private var isListVisible: Bool = false + + @State private var zoomScale: CGFloat = 1.0 + + + var flipCameraButton: some View { + CameraSettingButton(action: { + cameraSettingMode = .SWITCH_LENS + isListVisible = false + if(isFlipped){ + withAnimation(Animation.linear(duration: 0.3)) { + self.rotation = -90.0 + } + isFlipped = false + } else { + withAnimation(Animation.linear(duration: 0.3)) { + self.rotation = 90.0 + } + isFlipped = true + } + + cameraSettingModel.switchCamera() + }, image: "flip_camera", size: 30).rotationEffect(.degrees(rotation)) + } + + var zoomSlider: some View { + CameraSettingButton(action: { + cameraSettingMode = .ZOOM + isListVisible.toggle() + zoomClicked = !zoomClicked + }, image: "ic_zoom", size: 30) + } + + var autoExposure: some View { + CameraSettingButton(action: { + cameraSettingMode = .AUTO_EXPOSURE + autoExposureOff = !autoExposureOff + isListVisible = false + zoomClicked = false + cameraSettingModel.autoExposure(auto: !autoExposureOff) + }, image: "ic_auto_exposure", size: 25) + } + + var exposureSetting: some View{ + CameraSettingButton(action: { + cameraSettingMode = .EXPOSURE + isListVisible.toggle() + zoomClicked = false + }, image: "ic_exposure", size: 30) + } + + var isoSetting: some View { + CameraSettingButton(action: { + cameraSettingMode = .ISO + isListVisible.toggle() + zoomClicked = false + }, image: "ic_camera_iso", size: 30) + } + + var shutterSpeedSetting: some View { + CameraSettingButton(action: { + cameraSettingMode = .SHUTTER_SPEED + isListVisible.toggle() + zoomClicked = false + }, image: "ic_shutter_speed", size: 30) + } + + var apertureSetting: some View { + + Button(action: { + cameraSettingMode = .NONE + isListVisible = false + zoomClicked = false + }){ + Circle() + .foregroundColor(Color.gray.opacity(0.0)) + .frame(width: 45, height: 45, alignment: .center) + .overlay( + Image(systemName: "camera.aperture") + .resizable() + .scaledToFit() + .frame(width: 25, height: 25) + .clipShape(Circle()) + ) + } + + } + + + var body: some View { + + GeometryReader { reader in + let _ = print("setting height", reader.size.width) + let _ = print("setting width", reader.size.width) + } + if(exposureSettingLevel == 0){ + HStack{} + } else { + if(cameraSettingMode == .ISO || cameraSettingMode == .SHUTTER_SPEED){ + Spacer().frame(width: 10.0, height: 40.0) + } else { + Spacer().frame(width: 0.0, height: 0.0) + } + + + HStack { + + VStack { + flipCameraButton.frame(maxWidth: .infinity) + Text(isFlipped ? "Front" : "Back").font(.caption2) + } + + if(exposureSettingLevel == 2){ + VStack { + exposureSetting + .opacity(autoExposureOff ? 1.0 : 0.4 ) + .disabled(autoExposureOff ? false : true).frame(maxWidth: .infinity) + + Text("0.0") + .font(.caption2) + .opacity(autoExposureOff ? 1.0 : 0.4 ) + } + + } + + if(exposureSettingLevel == 3){ + VStack { + autoExposure.frame(maxWidth: .infinity) + Text(autoExposureOff ? "Off" : "On").font(.caption2) + } + + VStack { + isoSetting + .opacity(autoExposureOff ? 1.0 : 0.4 ) + .disabled(autoExposureOff ? false : true) + .frame(maxWidth: .infinity) + + Text(String(Int(cameraSettingModel.currentIso))) + .font(.caption2) + .opacity(autoExposureOff ? 1.0 : 0.4 ) + } + + + VStack { + + shutterSpeedSetting + .opacity(autoExposureOff ? 1.0 : 0.4 ) + .disabled(autoExposureOff ? false : true) + .frame(maxWidth: .infinity) + + let exposureDuration = CMTimeGetSeconds(cameraSettingModel.currentShutterSpeed ?? CMTime(seconds: 30.0, preferredTimescale: 1000)) + + Text("1/" + String(Int((1 / exposureDuration)))) + .font(.caption2) + .opacity(autoExposureOff ? 1.0 : 0.4 ) + } + + VStack { + + apertureSetting + .opacity(autoExposureOff ? 1.0 : 0.4 ) + .frame(maxWidth: .infinity) + .disabled(autoExposureOff ? false : true) + + Text("f/" + String(cameraSettingModel.currentApertureValue)) + .font(.caption2) + .opacity(autoExposureOff ? 1.0 : 0.4 ) + } + } + + VStack { + zoomSlider.frame(maxWidth: .infinity) + Text("Zoom").font(.caption2) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal, 1) + .preferredColorScheme(.dark) + + } + + + VStack { + var cameraSettingsValues = cameraSettingModel.getLisOfCameraSettingsValue(cameraSettingMode: cameraSettingMode) + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + if(cameraSettingMode == .ISO || cameraSettingMode == .SHUTTER_SPEED){ + Spacer().frame(width: 10.0, height: 40.0) + } else { + Spacer().frame(width: 0.0, height: 0.0) + } + ForEach(cameraSettingsValues , id: \.self) { title in + if(cameraSettingMode == .SHUTTER_SPEED){ + TextButton(text: "1/" + String(Int(title))) { + cameraSettingModel.shutterSpeed(value: Double(title)) + + } + } else if(cameraSettingMode == .ISO){ + TextButton(text: String(Int(title))) { + cameraSettingModel.iso(value: Int(title)) + } + + } else if(cameraSettingMode == .EXPOSURE){ + TextButton(text: String(title)) { + cameraSettingModel.exposure(value: title) + + } + } + } + } + }.opacity(!isListVisible ? 0.0 : 1.0) + + Spacer().frame(width: 10.0, height: 10.0) + + let sliderRange: ClosedRange = Double(1)...Double((cameraSettingModel.defaultCameraSetting?.virtualDeviceSwitchOverVideoZoomFactors.last?.intValue ?? 1) * 3) + + Slider( + value: Binding(get: { + Double(self.cameraSettingModel.getScale()) + }, set: { newValue in + self.cameraSettingModel.setScale(scale: CGFloat(newValue)) + }), + in: sliderRange, + step: 0.1 + ) { + Text("") + } minimumValueLabel: { + Text(String(cameraSettingModel.minZoom)) + } maximumValueLabel: { + Text(String(cameraSettingModel.maxZoom)) + } onEditingChanged: { editing in + isEditing = editing + }.opacity(zoomClicked ? 1.0 : 0.0) + } + + } +} + +public struct AlertError { + public var title: String = "" + public var message: String = "" + public var primaryButtonTitle = "Accept" + public var secondaryButtonTitle: String? + public var primaryAction: (() -> ())? + public var secondaryAction: (() -> ())? + + public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) { + self.title = title + self.message = message + self.primaryAction = primaryAction + self.primaryButtonTitle = primaryButtonTitle + self.secondaryAction = secondaryAction + } +} + +@available(iOS 13.0.0, *) +struct TextButton: View { + + @Environment(\.colorScheme) var colorScheme + var text: String + var action: () -> Void + + @available(iOS 13.0.0, *) + var body: some View { + Button(action: action) { + Text(text) + .padding(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8)) + .foregroundColor(.black) + .background(Color("buttonBackground")) + .cornerRadius(10) + } + } +} + + + diff --git a/phyphox-iOS/phyphox/Camera/ExperimentCameraInput.swift b/phyphox-iOS/phyphox/Camera/ExperimentCameraInput.swift new file mode 100644 index 00000000..3d755c5d --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/ExperimentCameraInput.swift @@ -0,0 +1,110 @@ +// +// ExperimentCameraInput.swift +// phyphox +// +// Created by Gaurav Tripathee on 12.02.24. +// Copyright © 2024 RWTH Aachen. All rights reserved. +// + +import Foundation + +final class ExperimentCameraInput { + + let initx1: Float + let initx2: Float + let inity1: Float + let inity2: Float + + let timeReference: ExperimentTimeReference? + let zBuffer: DataBuffer? + let tBuffer: DataBuffer? + + let autoExposure: Bool + let exposureAdjustmentLevel: Int + let locked: String + let feature: String + let analysis: String + + lazy var session: Any? = nil + + + init(timeReference: ExperimentTimeReference, zBuffer: DataBuffer?, tBuffer: DataBuffer?, x1: Float, x2: Float, y1: Float, y2: Float, smooth: Bool, autoExposure: Bool, exposureAdjustmentLevel: Int, locked: String, feature: String, analysis: String) { + self.initx1 = x1 + self.initx2 = x2 + self.inity1 = y1 + self.inity2 = y2 + self.zBuffer = zBuffer + self.tBuffer = tBuffer + self.timeReference = timeReference + self.autoExposure = autoExposure + self.exposureAdjustmentLevel = exposureAdjustmentLevel + self.locked = locked + self.feature = feature + self.analysis = analysis + + session = ExperimentCameraInputSession() + guard let session = session as? ExperimentCameraInputSession else { + return + } + + session.x1 = x1 + session.x2 = x2 + session.y1 = y1 + session.y2 = y2 + session.zBuffer = zBuffer + session.tBuffer = tBuffer + session.timeReference = timeReference + + session.autoExposure = autoExposure + session.exposureAdjustmentLevel = exposureAdjustmentLevel + session.locked = locked + session.feature = feature + session.analysis = analysis + + + } + + func start() throws { + guard #available(iOS 14.0, *) else { + return + } + guard let session = session as? ExperimentCameraInputSession else { + return + } + + session.startSession() + } + + func stop() { + guard #available(iOS 14.0, *) else { + return + } + guard let session = session as? ExperimentCameraInputSession else { + return + } + session.stopSession() + } + + func clear() { + guard #available(iOS 14.0, *) else { + return + } + guard let session = session as? ExperimentCameraInputSession else { + return + } + session.clear() + } + +} + +extension ExperimentCameraInput: Equatable { + static func ==(lhs: ExperimentCameraInput, rhs: ExperimentCameraInput) -> Bool { + return lhs.initx1 == rhs.initx1 && + lhs.initx2 == rhs.initx2 && + lhs.inity1 == rhs.inity1 && + lhs.inity2 == rhs.inity2 && + lhs.timeReference == rhs.timeReference && + lhs.zBuffer == rhs.zBuffer && + lhs.tBuffer == rhs.tBuffer + } +} diff --git a/phyphox-iOS/phyphox/Camera/ExperimentCameraInputSession.swift b/phyphox-iOS/phyphox/Camera/ExperimentCameraInputSession.swift new file mode 100644 index 00000000..95ce9a27 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/ExperimentCameraInputSession.swift @@ -0,0 +1,108 @@ +// +// ExperimentCameraInputSession.swift +// phyphox +// +// Created by Gaurav Tripathee on 14.02.24. +// Copyright © 2024 RWTH Aachen. All rights reserved. +// + +import Foundation + +class ExperimentCameraInputSession: NSObject { + var timeReference: ExperimentTimeReference? + + var zBuffer: DataBuffer? + + var tBuffer: DataBuffer? + + var x1: Float = 0.4 + var x2: Float = 0.6 + var y1: Float = 0.4 + var y2: Float = 0.6 + + lazy var cameraModel: Any? = nil + + var autoExposure: Bool = true + var exposureAdjustmentLevel: Int = 0 + var locked: String = "" + var feature: String = "" + var analysis: String = "" + + var delegate : CameraGUIDelegate? + + func initializeCameraModel(){ + + + if #available(iOS 14.0, *) { + cameraModel = CameraModel() + + guard let cameraModel = cameraModel as? CameraModel else { + return + } + + cameraModel.x1 = x1 + cameraModel.x2 = x2 + cameraModel.y1 = y1 + cameraModel.y2 = y2 + + cameraModel.metalRenderer.timeReference = timeReference + cameraModel.metalRenderer.zBuffer = zBuffer + cameraModel.metalRenderer.tBuffer = tBuffer + + cameraModel.exposureSettingLevel = exposureAdjustmentLevel + } else { + // Fallback on earlier versions + } + } + + func startSession(){ + if #available(iOS 14.0, *) { + guard let cameraModel = cameraModel as? CameraModel else { + return + } + + cameraModel.startSession() + + } else { + // Fallback on earlier versions + } + } + + func stopSession(){ + if #available(iOS 14.0, *) { + guard let cameraModel = cameraModel as? CameraModel else { + return + } + + cameraModel.stopSession() + + } else { + // Fallback on earlier versions + } + } + + func endSession(){ + if #available(iOS 14.0, *) { + guard let cameraModel = cameraModel as? CameraModel else { + return + } + + cameraModel.endSession() + + } else { + // Fallback on earlier versions + } + } + + + func clear() { + + } + + + public func attachDelegate(delegate: CameraGUIDelegate) { + self.delegate = delegate + delegate.updateResolution(resolution: CGSize(width: 300, height: 300)) + } + +} diff --git a/phyphox-iOS/phyphox/Camera/MetalRenderer.swift b/phyphox-iOS/phyphox/Camera/MetalRenderer.swift new file mode 100644 index 00000000..a51807c2 --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/MetalRenderer.swift @@ -0,0 +1,434 @@ +// +// MetalRenderer.swift +// phyphox +// +// Created by Gaurav Tripathee on 24.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation +import MetalKit +import AVFoundation +import Accelerate + + +@available(iOS 13.0, *) +class MetalRenderer: NSObject, MTKViewDelegate{ + + var metalDevice: MTLDevice? + var metalCommandQueue: MTLCommandQueue! + var imagePlaneVertexBuffer: MTLBuffer! + var renderDestination: MTKView + + // The current viewport size. + var viewportSize: CGSize = CGSize() + + // Flag for viewport size changes. + var viewportSizeDidChange: Bool = false + + // An object that defines the Metal shaders that render the camera image. + var pipelineState: MTLRenderPipelineState! + + // Captured image texture cache. + var cameraImageTextureCache: CVMetalTextureCache! + + let inFlightSemaphore = DispatchSemaphore(value: kMaxBuffersInFlight) + + var displayToCameraTransform: CGAffineTransform = .identity + var selectionState = SelectionStruct(x1: 0.4, x2: 0.6, y1: 0.4, y2: 0.6, editable: false) + + + // Textures used to transfer the current camera image to the GPU for rendering. + var cameraImageTextureY: CVMetalTexture? + var cameraImageTextureCbCr: CVMetalTexture? + + var cvImageBuffer : CVImageBuffer? + + var measuring: Bool = false + + + var timeReference: ExperimentTimeReference? + var zBuffer: DataBuffer? + var tBuffer: DataBuffer? + + private var queue: DispatchQueue? + + struct SelectionStruct { + var x1, x2, y1, y2: Float + var editable: Bool + } + + + init(parent: CameraMetalView ,renderer: MTKView) { + + self.renderDestination = renderer + + if let metalDevice = MTLCreateSystemDefaultDevice() { + self.metalDevice = metalDevice + } + + super.init() + + loadMetal() + + } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + drawRectResized(size: size) + } + + func draw(in view: MTKView) { + update() + } + + + + // Schedule a draw to happen at a new size. + func drawRectResized(size: CGSize) { + viewportSize = size + viewportSizeDidChange = true + } + + + func updateFrame(imageBuffer: CVImageBuffer!, selectionState: SelectionStruct, time: TimeInterval) { + + if imageBuffer != nil { + + self.cvImageBuffer = imageBuffer + } + + self.selectionState = selectionState + + if measuring { + getLuma(time: time) + } + + } + + func start(queue: DispatchQueue) throws { + self.queue = queue + } + + func getLuma(time: Double){ + if let pixelBuffer = self.cvImageBuffer{ + + var luma = 0 + let luminance = 00 + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags.readOnly) + let lumaBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) + let lumaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) + let lumaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) + let lumaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) + + var lData = vImage_Buffer(data: lumaBaseAddress, height: vImagePixelCount(lumaHeight), width: vImagePixelCount(lumaWidth), rowBytes: lumaRowBytes) + + + let chromaBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) + let chromaWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1) + let chromaHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1) + let chromaRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) + + var cData = vImage_Buffer(data: chromaBaseAddress, height: vImagePixelCount(chromaHeight), width: vImagePixelCount(chromaWidth), rowBytes: chromaRowBytes) + + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags.readOnly) + + let byteBuffer = UnsafeMutableRawPointer(lumaBaseAddress) + + let bufferPointer = byteBuffer?.assumingMemoryBound(to: Pixel_8.self) + + + for y in 0...lData.height { + for x in 0...lData.width { + var l = (bufferPointer?[Int(x)] ?? 0) & 0xFF + luma += Int(l) + + } + } + + let xmin = Int(self.selectionState.x1 * Float(lData.width)) + let xmax = Int(self.selectionState.x2 * Float(lData.width)) + let ymin = Int(self.selectionState.y1 * Float(lData.height)) + let ymax = Int(self.selectionState.y2 * Float(lData.height)) + + + let analysisArea = (xmax - xmin) * (ymax - ymin) + + luma /= analysisArea * 255 + + dataIn(z: Double(luma), time: time) + + + } else { + // If the CVImageBuffer is not a CVPixelBuffer, you may need to perform conversion + + } + } + + + private func writeToBuffers(z: Double, t: TimeInterval) { + if let zBuffer = zBuffer { + zBuffer.append(z) + } + + if let tBuffer = tBuffer { + tBuffer.append(t) + } + } + + private func dataIn(z: Double, time: TimeInterval) { + guard let zBuffer = zBuffer else { + print("Error: zBuffer not set") + return + } + guard let tBuffer = tBuffer else { + print("Error: tBuffer not set") + return + } + guard let timeReference = timeReference else { + print("Error: time reference not set") + return + } + + let t = timeReference.getExperimentTimeFromEvent(eventTime: time) + + if t >= timeReference.timeMappings.last?.experimentTime ?? 0.0 { + self.writeToBuffers(z: z, t: t) + } + } + + func loadMetal(){ + + // Set the default formats needed to render. + renderDestination.colorPixelFormat = .bgra8Unorm + renderDestination.sampleCount = 1 + + // Create a vertex buffer with our image plane vertex data. + let imagePlaneVertexDataCount = kImagePlaneVertexData.count * MemoryLayout.size + imagePlaneVertexBuffer = metalDevice?.makeBuffer(bytes: kImagePlaneVertexData, length: imagePlaneVertexDataCount, options: []) + imagePlaneVertexBuffer.label = "ImagePlaneVertexBuffer" + + // Load all the shader files with a metal file extension in the project. + let defaultLibrary = metalDevice?.makeDefaultLibrary()! + + // Create a vertex descriptor for our image plane vertex buffer. + let imagePlaneVertexDescriptor = MTLVertexDescriptor() + + // Positions. + imagePlaneVertexDescriptor.attributes[0].format = .float2 + imagePlaneVertexDescriptor.attributes[0].offset = 0 + imagePlaneVertexDescriptor.attributes[0].bufferIndex = Int(kBufferIndexMeshPositions.rawValue) + + // Texture coordinates. + imagePlaneVertexDescriptor.attributes[1].format = .float2 + imagePlaneVertexDescriptor.attributes[1].offset = 8 + imagePlaneVertexDescriptor.attributes[1].bufferIndex = Int(kBufferIndexMeshPositions.rawValue) + + // Buffer Layout. + imagePlaneVertexDescriptor.layouts[0].stride = 16 + imagePlaneVertexDescriptor.layouts[0].stepRate = 1 + imagePlaneVertexDescriptor.layouts[0].stepFunction = .perVertex + + // Create camera image texture cache. + var textureCache: CVMetalTextureCache? + CVMetalTextureCacheCreate(nil, nil, metalDevice!, nil, &textureCache) + cameraImageTextureCache = textureCache + + // Define the shaders that will render the camera image on the GPU. + let vertexFunction = defaultLibrary?.makeFunction(name: "vertexTransform")! + let fragmentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")! + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.label = "MyPipeline" + pipelineStateDescriptor.sampleCount = renderDestination.sampleCount + pipelineStateDescriptor.vertexFunction = vertexFunction + pipelineStateDescriptor.fragmentFunction = fragmentFunction + pipelineStateDescriptor.vertexDescriptor = imagePlaneVertexDescriptor + pipelineStateDescriptor.colorAttachments[0].pixelFormat = renderDestination.colorPixelFormat + + // Initialize the pipeline. + do { + try pipelineState = metalDevice?.makeRenderPipelineState(descriptor: pipelineStateDescriptor) + } catch let error { + print("Failed to create pipeline state, error \(error)") + } + + // Create the command queue for one frame of rendering work. + metalCommandQueue = metalDevice?.makeCommandQueue() + + + + } + + + + func update() { + + // Wait to ensure only kMaxBuffersInFlight are getting proccessed by any stage in the Metal + // pipeline (App, Metal, Drivers, GPU, etc). + _ = inFlightSemaphore.wait(timeout: DispatchTime.distantFuture) + + // Create a new command buffer for each renderpass to the current drawable. + if let commandBuffer = metalCommandQueue.makeCommandBuffer() { + commandBuffer.label = "MyCommand" + + // Add completion hander which signal _inFlightSemaphore when Metal and the GPU has fully + // finished proccssing the commands we're encoding this frame. This indicates when the + // dynamic buffers, that we're writing to this frame, will no longer be needed by Metal + // and the GPU. + commandBuffer.addCompletedHandler { [weak self] commandBuffer in + if let strongSelf = self { + strongSelf.inFlightSemaphore.signal() + } + } + + updateAppState() + + + + if let renderPassDescriptor = renderDestination.currentRenderPassDescriptor, let currentDrawable = renderDestination.currentDrawable { + + if let renderEncoding = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) { + + // Set a label to identify this render pass in a captured Metal frame. + renderEncoding.label = "DepthGUICameraPreview" + + // Schedule the camera image to be drawn to the screen. + doRenderPass(renderEncoder: renderEncoding) + + // Finish encoding commands. + renderEncoding.endEncoding() + } + + // Schedule a present once the framebuffer is complete using the current drawable. + commandBuffer.present(currentDrawable) + } + + // Finalize rendering here & push the command buffer to the GPU. + commandBuffer.commit() + + + } + } + + // Schedules the camera image to be rendered on the GPU. + func doRenderPass(renderEncoder: MTLRenderCommandEncoder) { + + guard let cameraImageY = cameraImageTextureY, let cameraImageCbCr = cameraImageTextureCbCr else { + return + } + + // Push a debug group that enables you to identify this render pass in a Metal frame capture. + renderEncoder.pushDebugGroup("CameraPass") + + // Set render command encoder state. + renderEncoder.setCullMode(.none) + renderEncoder.setRenderPipelineState(pipelineState) + + let p1 = CGPoint(x: CGFloat(selectionState.x1), y: CGFloat(selectionState.y1)).applying(displayToCameraTransform.inverted()) + let p2 = CGPoint(x: CGFloat(selectionState.x2), y: CGFloat(selectionState.y2)).applying(displayToCameraTransform.inverted()) + var scaledSelectionState = SelectionStruct(x1: Float(min(p1.x, p2.x)*viewportSize.width), x2: Float(max(p1.x, p2.x)*viewportSize.width), y1: Float(min(p1.y, p2.y)*viewportSize.height), y2: Float(max(p1.y, p2.y)*viewportSize.height), editable: selectionState.editable) + renderEncoder.setFragmentBytes(&scaledSelectionState, length: MemoryLayout.stride, index: 2) + + // Setup plane vertex buffers. + renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 0) + renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 1) + + // Setup textures for the camera fragment shader. + renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageY), index: 0) + renderEncoder.setFragmentTexture(CVMetalTextureGetTexture(cameraImageCbCr), index: 1) + + // Draw final quad to display + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + renderEncoder.popDebugGroup() + + + } + + // Updates any app state. + func updateAppState() { + + + + guard let currentFrame = self.cvImageBuffer else { + return + } + + + // Prepare the current frame's camera image for transfer to the GPU. + updateCameraImageTextures(frame: currentFrame) + + // Update the destination-rendering vertex info if the size of the screen changed. + if viewportSizeDidChange { + viewportSizeDidChange = false + updateImagePlane(frame: currentFrame) + } + } + + // Creates two textures (Y and CbCr) to transfer the current frame's camera image to the GPU for rendering. + func updateCameraImageTextures(frame: CVImageBuffer) { + if CVPixelBufferGetPlaneCount(frame) < 2 { + print("updateCameraImageTextures less than 2") + return + } + cameraImageTextureY = createTexture(fromPixelBuffer: frame, pixelFormat: .r8Unorm, planeIndex: 0) + cameraImageTextureCbCr = createTexture(fromPixelBuffer: frame, pixelFormat: .rg8Unorm, planeIndex: 1) + } + + // Creates a Metal texture with the argument pixel format from a CVPixelBuffer at the argument plane index. + func createTexture(fromPixelBuffer pixelBuffer: CVImageBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> CVMetalTexture? { + + let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex) // 480 //240 + let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex) // 360 //180 + + var texture: CVMetalTexture? = nil + let status = CVMetalTextureCacheCreateTextureFromImage(nil, cameraImageTextureCache, pixelBuffer, nil, pixelFormat, + width, height, planeIndex, &texture) + + if status != kCVReturnSuccess { + texture = nil + } + + return texture + } + + // Sets up vertex data (source and destination rectangles) rendering. + func updateImagePlane(frame: CVImageBuffer) { + // Update the texture coordinates of the image plane to aspect fill the viewport. + //let orientation = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.windowScene?.interfaceOrientation ?? .portrait + //let cgImageOrientation = CGImagePropertyOrientation(interfaceOrientation: orientation) + // Convert the image buffer to a CIImage + //let ciImage = CIImage(cvPixelBuffer: frame) + //displayToCameraTransform = ciImage.orientationTransform(for: cgImageOrientation).inverted() + + // Just a work-around + displayToCameraTransform = CGAffineTransform(a: 0.0, b: -1.0, c: 1.0, d: 0.0, tx: 0.0, ty: 1.0) + let vertexData = imagePlaneVertexBuffer.contents().assumingMemoryBound(to: Float.self) + for index in 0...3 { + let textureCoordIndex = 4 * index + 2 + let textureCoord = CGPoint(x: CGFloat(kImagePlaneVertexData[textureCoordIndex]), y: CGFloat(kImagePlaneVertexData[textureCoordIndex + 1])) + let transformedCoord = textureCoord.applying(displayToCameraTransform) + vertexData[textureCoordIndex] = Float(transformedCoord.x) + vertexData[textureCoordIndex + 1] = Float(transformedCoord.y) + } + } + + + +} + +extension CGImagePropertyOrientation { + init(interfaceOrientation: UIInterfaceOrientation) { + switch interfaceOrientation { + case .portrait: + self = .up + case .portraitUpsideDown: + self = .down + case .landscapeLeft: + self = .left + case .landscapeRight: + self = .right + default: + self = .up + } + } +} diff --git a/phyphox-iOS/phyphox/Camera/MetalView.swift b/phyphox-iOS/phyphox/Camera/MetalView.swift new file mode 100644 index 00000000..f1d087ac --- /dev/null +++ b/phyphox-iOS/phyphox/Camera/MetalView.swift @@ -0,0 +1,48 @@ +// +// MetalView.swift +// phyphox +// +// Created by Gaurav Tripathee on 24.11.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation +import MetalKit +import SwiftUI + + +@available(iOS 13.0, *) +struct CameraMetalView: UIViewRepresentable { + let metalView: MTKView = MTKView() + + func makeUIView(context: UIViewRepresentableContext) -> MTKView { + print("MetalView: makeUIView") + //metalView.delegate = context.coordinator + + metalView.preferredFramesPerSecond = 60 + + if let metalDevice = MTLCreateSystemDefaultDevice() { + metalView.device = metalDevice + } + + metalView.preferredFramesPerSecond = 60 + + //metalView.framebufferOnly = false + + metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) + + //Fix: As the imagebuffer width * height is 480 and 180, we need to set the drawable size of MTKView same. Else, the drawable size will be 1080 * 1256 (for example) and due to the large render buffer, the frame starts dropping. TODO: Need to test it and remove the hard dependency. + + let h = metalView.drawableSize.height + let w = metalView.drawableSize.width + metalView.drawableSize = CGSize(width: w / 2, height: h / 2) + + return metalView + } + + func updateUIView(_ uiView: MTKView, context: UIViewRepresentableContext) { + + } + +} + diff --git a/phyphox-iOS/phyphox/Constants.swift b/phyphox-iOS/phyphox/Constants.swift index 43728d96..4a0fb32d 100644 --- a/phyphox-iOS/phyphox/Constants.swift +++ b/phyphox-iOS/phyphox/Constants.swift @@ -55,6 +55,10 @@ let namedColors = [ "weakwhite": UIColor(red: (196.0/255.0), green: (196.0/255.0), blue: (196.0/255.0), alpha: 1.0) ] +let shutters = [1, 2, 4, 8, 15, 30, 60, 125, 250, 500, 1000, 2000, 4000, 8000] + +let iso = [25, 50, 100, 200, 400, 800, 1600, 3200, 6400, 12800, 25600, 51200] + func mapColorString(_ string: String?) -> UIColor? { guard let colorString = string else { return nil diff --git a/phyphox-iOS/phyphox/Experiments/BluetoothDevice.swift b/phyphox-iOS/phyphox/Experiments/BluetoothDevice.swift index 23120d04..bc77718a 100644 --- a/phyphox-iOS/phyphox/Experiments/BluetoothDevice.swift +++ b/phyphox-iOS/phyphox/Experiments/BluetoothDevice.swift @@ -14,6 +14,7 @@ let phyphoxServiceUUID: UUID = UUID(uuidString: "cddf0001-30f7-4671-8b43-5e40ba5 let phyphoxExperimentCharacteristicUUID: UUID = UUID(uuidString: "cddf0002-30f7-4671-8b43-5e40ba53514a")! let phyphoxExperimentControlCharacteristicUUID: UUID = UUID(uuidString: "cddf0003-30f7-4671-8b43-5e40ba53514a")! let phyphoxEventCharacteristicUUID: UUID = UUID(uuidString: "cddf0004-30f7-4671-8b43-5e40ba53514a")! +let batteryServiceUUID: String = "2A19" public extension CBUUID { convenience init(uuidString: String) throws { @@ -76,6 +77,10 @@ protocol BluetoothDeviceDelegate { func dataFromCharacteristic(uuid: CBUUID, data: Data) } +protocol UpdateConnectedDeviceDelegate { + func showUpdatedConnectedDevices(connectedDevice: [ConnectedDevicesDataModel]) +} + enum BluetoothDeviceError: Error { case generic(String) } @@ -100,6 +105,26 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { let hud: JGProgressHUD var feedbackViewController: UIViewController? + static var updateDelegate: UpdateConnectedDeviceDelegate? + + var batteryLevel = -1 + var connectedDeviceName: String = "" + var signalLevel = 100 + var connectedDevices: [ConnectedDevicesDataModel] = [ConnectedDevicesDataModel]() + + init(delegate: UpdateConnectedDeviceDelegate) { + ExperimentBluetoothDevice.updateDelegate = delegate + + self.id = "" + self.deviceName = "" + self.advertiseUUID = nil + + self.hud = JGProgressHUD(style: .dark) + + super.init(scanDirectly: false, filterByName: "", filterByUUID: nil, checkExperiments: false, autoConnect: false) + } + + init(id: String?, name: String?, uuid: CBUUID?, autoConnect: Bool) { self.id = id self.deviceName = name @@ -129,7 +154,7 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { let alert = UIAlertController(title: localize("warning"), message: msg, preferredStyle: .alert) alert.addAction(UIAlertAction(title: localize("cancel"), style: .default, handler: { _ in - + })) if retry { alert.addAction(UIAlertAction(title: localize("tryagain"), style: .default, handler: { _ in @@ -274,7 +299,9 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } characteristics_map = [:] servicesToBeDiscovered = [] + connectedDevices = [] centralManager?.stopScan() + } override func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { @@ -288,7 +315,7 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { override func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("Connected: \(peripheral.name ?? "No Name")") - + peripheral.readRSSI() peripheral.discoverServices(nil) after(10) { if self.characteristics_map.count == 0 { @@ -299,6 +326,36 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } } + func peripheral(_ peripheral: CBPeripheral,didReadRSSI RSSI: NSNumber, error: Error?){ + signalLevel = Int(RSSI) + connectedDeviceName = peripheral.name ?? "" + let connectedDevice = ConnectedDevicesDataModel(deviceIdentifier: peripheral.identifier, + signalStrength: signalLevel, + batteryLabel: batteryLevel, + deviceName: connectedDeviceName) + + // append or update the list as per the current state + if(connectedDevices.isEmpty){ + connectedDevices.append(connectedDevice) + + } else { + if(connectedDevices.filter { $0.getDeviceIdentifier() == peripheral.identifier}.isEmpty){ + // if the current device name is not in list than add it to the list + connectedDevices.append(connectedDevice) + } else{ + // Get row index of the current device info to update its elements + if let row = connectedDevices.firstIndex(where: {$0.getDeviceIdentifier() == peripheral.identifier}){ + connectedDevices[row] = connectedDevice + } + } + } + + ExperimentBluetoothDevice + .updateDelegate? + .showUpdatedConnectedDevices(connectedDevice: connectedDevices) + + } + override func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { self.disconnect() self.showError(msg: localize("bt_exception_connection")) @@ -365,8 +422,19 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + peripheral.readRSSI() if let newData = characteristic.value { for delegate in delegates { + if(characteristic.uuid.uuidString == batteryServiceUUID){ + guard let newBatteryLevel = characteristic.value?.first else { + return + } + if(batteryLevel != newBatteryLevel) { + batteryLevel = Int(newBatteryLevel) + peripheral.readValue(for: characteristic) + } + + } delegate.dataFromCharacteristic(uuid: characteristic.uuid, data: newData) } } @@ -374,10 +442,10 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { /* We ignore errors because it seems possible that devices do not set up a proper config descriptor while still working as expected. - if let error = error { - self.disconnect() - self.showError(msg: localize("bt_exception_notification_fail_enable") + " \(characteristic.uuid) " + localize("bt_exception_notification_fail") + " (\(error.localizedDescription))") - } + if let error = error { + self.disconnect() + self.showError(msg: localize("bt_exception_notification_fail_enable") + " \(characteristic.uuid) " + localize("bt_exception_notification_fail") + " (\(error.localizedDescription))") + } */ } @@ -393,9 +461,18 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } for characteristic in characteristics { + if characteristic.uuid.uuid128String == phyphoxEventCharacteristicUUID.uuidString { eventCharacteristic = characteristic } + + if(characteristic.uuid.uuidString == batteryServiceUUID){ + //Battery Label + peripheral.readValue(for: characteristic) + + } + + characteristics_map[characteristic.uuid.uuid128String] = characteristic } @@ -405,10 +482,10 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } print("Service discovery completed. Writing config data to device.") - + do { writeEventCharacteristic(timeMapping: nil) - + for delegate in delegates { try delegate.writeConfigData() } @@ -473,3 +550,40 @@ class ExperimentBluetoothDevice: BluetoothScan, DeviceIsChosenDelegate { } +class ConnectedDevicesDataModel { + private var deviceIdentifier: UUID? + private var signalStrength: Int? + private var batteryLabel: Int? + private var deviceName: String? + + init(deviceIdentifier: UUID, signalStrength: Int, batteryLabel: Int, deviceName: String) { + self.deviceIdentifier = deviceIdentifier + self.signalStrength = signalStrength + self.batteryLabel = batteryLabel + self.deviceName = deviceName + } + + init(signalStrength: Int) { + self.signalStrength = signalStrength + } + + func getDeviceIdentifier() -> UUID { + return deviceIdentifier ?? UUID() + } + + func getSignalStrength() -> Int{ + return signalStrength ?? 100 + } + + func getBatteryLabel() -> Int{ + return batteryLabel ?? 100 + } + + func getDeviceName() -> String{ + return deviceName ?? "" + } + +} + + + diff --git a/phyphox-iOS/phyphox/Experiments/Data-Input/ExperimentDepthInput.swift b/phyphox-iOS/phyphox/Experiments/Data-Input/ExperimentDepthInput.swift index 345cd3a9..cd036635 100644 --- a/phyphox-iOS/phyphox/Experiments/Data-Input/ExperimentDepthInput.swift +++ b/phyphox-iOS/phyphox/Experiments/Data-Input/ExperimentDepthInput.swift @@ -61,7 +61,7 @@ final class ExperimentDepthInput { session.mode = mode session.x1 = x1 session.x2 = x2 - session.y1 = y1 + session.y1 = y1 session.y2 = y2 session.zBuffer = zBuffer session.tBuffer = tBuffer diff --git a/phyphox-iOS/phyphox/Experiments/Experiment.swift b/phyphox-iOS/phyphox/Experiments/Experiment.swift index ec50498e..71997016 100644 --- a/phyphox-iOS/phyphox/Experiments/Experiment.swift +++ b/phyphox-iOS/phyphox/Experiments/Experiment.swift @@ -91,6 +91,7 @@ final class Experiment { let sensorInputs: [ExperimentSensorInput] let depthInput: ExperimentDepthInput? + let cameraInput: ExperimentCameraInput? let gpsInputs: [ExperimentGPSInput] let audioInputs: [ExperimentAudioInput] @@ -116,7 +117,7 @@ final class Experiment { private let queue = DispatchQueue(label: "de.rwth-aachen.phyphox.analysis", attributes: []) - init(title: String, stateTitle: String?, description: String?, links: [ExperimentLink], category: String, icon: ExperimentIcon, color: UIColor?, appleBan: Bool, isLink: Bool, translation: ExperimentTranslationCollection?, buffers: [String: DataBuffer], timeReference: ExperimentTimeReference, sensorInputs: [ExperimentSensorInput], depthInput: ExperimentDepthInput?, gpsInputs: [ExperimentGPSInput], audioInputs: [ExperimentAudioInput], audioOutput: ExperimentAudioOutput?, bluetoothDevices: [ExperimentBluetoothDevice], bluetoothInputs: [ExperimentBluetoothInput], bluetoothOutputs: [ExperimentBluetoothOutput], networkConnections: [NetworkConnection], viewDescriptors: [ExperimentViewCollectionDescriptor]?, analysis: ExperimentAnalysis, export: ExperimentExport?) { + init(title: String, stateTitle: String?, description: String?, links: [ExperimentLink], category: String, icon: ExperimentIcon, color: UIColor?, appleBan: Bool, isLink: Bool, translation: ExperimentTranslationCollection?, buffers: [String: DataBuffer], timeReference: ExperimentTimeReference, sensorInputs: [ExperimentSensorInput], depthInput: ExperimentDepthInput?, cameraInput: ExperimentCameraInput?, gpsInputs: [ExperimentGPSInput], audioInputs: [ExperimentAudioInput], audioOutput: ExperimentAudioOutput?, bluetoothDevices: [ExperimentBluetoothDevice], bluetoothInputs: [ExperimentBluetoothInput], bluetoothOutputs: [ExperimentBluetoothOutput], networkConnections: [NetworkConnection], viewDescriptors: [ExperimentViewCollectionDescriptor]?, analysis: ExperimentAnalysis, export: ExperimentExport?) { self.title = title self.stateTitle = stateTitle @@ -141,6 +142,7 @@ final class Experiment { self.buffers = buffers self.sensorInputs = sensorInputs self.depthInput = depthInput + self.cameraInput = cameraInput self.gpsInputs = gpsInputs self.audioInputs = audioInputs @@ -172,7 +174,7 @@ final class Experiment { } convenience init(file: String, error: String) { - self.init(title: file, stateTitle: nil, description: error, links: [], category: localize("unknown"), icon: ExperimentIcon.string("!"), color: UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0), appleBan: false, isLink: false, translation: nil, buffers: [:], timeReference: ExperimentTimeReference(), sensorInputs: [], depthInput: nil, gpsInputs: [], audioInputs: [], audioOutput: nil, bluetoothDevices: [], bluetoothInputs: [], bluetoothOutputs: [], networkConnections: [], viewDescriptors: nil, analysis: ExperimentAnalysis(modules: [], sleep: 0.0, dynamicSleep: nil, onUserInput: false, requireFill: nil, requireFillThreshold: 1, requireFillDynamic: nil, timedRun: false, timedRunStartDelay: 0.0, timedRunStopDelay: 0.0, timeReference: ExperimentTimeReference(), sensorInputs: [], audioInputs: []), export: nil) + self.init(title: file, stateTitle: nil, description: error, links: [], category: localize("unknown"), icon: ExperimentIcon.string("!"), color: UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0), appleBan: false, isLink: false, translation: nil, buffers: [:], timeReference: ExperimentTimeReference(), sensorInputs: [], depthInput: nil, cameraInput: nil, gpsInputs: [], audioInputs: [], audioOutput: nil, bluetoothDevices: [], bluetoothInputs: [], bluetoothOutputs: [], networkConnections: [], viewDescriptors: nil, analysis: ExperimentAnalysis(modules: [], sleep: 0.0, dynamicSleep: nil, onUserInput: false, requireFill: nil, requireFillThreshold: 1, requireFillDynamic: nil, timedRun: false, timedRunStartDelay: 0.0, timedRunStopDelay: 0.0, timeReference: ExperimentTimeReference(), sensorInputs: [], audioInputs: []), export: nil) invalid = true; } @@ -362,6 +364,7 @@ final class Experiment { sensorInputs.forEach{ $0.configureMotionSession() } sensorInputs.forEach { $0.start(queue: queue) } try depthInput?.start(queue: queue) + try cameraInput?.start() gpsInputs.forEach { $0.start(queue: queue) } bluetoothInputs.forEach { $0.start(queue: queue) } networkConnections.forEach { $0.start() } @@ -380,6 +383,7 @@ final class Experiment { sensorInputs.forEach { $0.stop() } depthInput?.stop() + cameraInput?.stop() gpsInputs.forEach { $0.stop() } bluetoothInputs.forEach { $0.stop() } networkConnections.forEach { $0.stop() } @@ -407,6 +411,7 @@ final class Experiment { sensorInputs.forEach { $0.clear() } depthInput?.clear() + cameraInput?.clear() gpsInputs.forEach { $0.clear() } if byUser { diff --git a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/InputElementHandler.swift b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/InputElementHandler.swift index bb088901..bb709d4e 100644 --- a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/InputElementHandler.swift +++ b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/InputElementHandler.swift @@ -118,6 +118,78 @@ private final class DepthElementHandler: ResultElementHandler, LookupElementHand } } +struct CameraInputDescriptor: SensorDescriptor { + let x1: Float + let x2: Float + let y1: Float + let y2: Float + let smooth: Bool + let autoExposure: Bool + let exposureAdjustmentLevel: Int + let locked: String + let feature: String + let analysis: String + let outputs: [SensorOutputDescriptor] +} + +private final class CameraElementHandler: ResultElementHandler, LookupElementHandler { + var results = [CameraInputDescriptor]() + + private let outputHandler = SensorOutputElementHandler() + + var childHandlers: [String : ElementHandler] + + init() { + childHandlers = ["output": outputHandler] + } + + func startElement(attributes: AttributeContainer) throws {} + + // spelling in the xml should match with it + private enum Attribute: String, AttributeKey { + case mode + case x1 + case x2 + case y1 + case y2 + case smooth + case auto_exposure + case exposure_adjustment_level + case locked + case feature + case analysis + } + + func endElement(text: String, attributes: AttributeContainer) throws { + let attributes = attributes.attributes(keyedBy: Attribute.self) + + let x1user: Float = try attributes.optionalValue(for: .x1) ?? 0.4 + let x2user: Float = try attributes.optionalValue(for: .x2) ?? 0.6 + let y1user: Float = try attributes.optionalValue(for: .y1) ?? 0.4 + let y2user: Float = try attributes.optionalValue(for: .y2) ?? 0.6 + + //Careful: We will now switch from the user coordinate system to the camera coordinate system: x -> -y, y -> -x + let x1 = 1.0-y1user + let x2 = 1.0-y2user + let y1 = 1.0-x1user + let y2 = 1.0-x2user + + let smooth: Bool = try attributes.optionalValue(for: .smooth) ?? true + + let autoExposure: Bool = try attributes.optionalValue(for: .auto_exposure) ?? true + + let exposureAdjustmentLevel: Int = try attributes.optionalValue(for: .exposure_adjustment_level) ?? 3 + + let locked: String = try attributes.optionalValue(for: .locked) ?? "" + + let feature: String = try attributes.optionalString(for: .feature) ?? "" + + let analysis: String = try attributes.optionalString(for: .analysis) ?? "" + + results.append(CameraInputDescriptor(x1: x1, x2: x2, y1: y1, y2: y2, smooth: smooth, autoExposure: autoExposure, exposureAdjustmentLevel: exposureAdjustmentLevel, locked: locked, feature: feature, analysis: analysis, outputs: outputHandler.results)) + } +} + struct SensorInputDescriptor: SensorDescriptor { let sensor: SensorType let rate: Double @@ -413,7 +485,7 @@ private final class BluetoothElementHandler: ResultElementHandler, LookupElement } final class InputElementHandler: ResultElementHandler, LookupElementHandler, AttributelessElementHandler { - typealias Result = (sensors: [SensorInputDescriptor], depth: [DepthInputDescriptor], audio: [AudioInputDescriptor], location: [LocationInputDescriptor], bluetooth: [BluetoothInputBlockDescriptor]) + typealias Result = (sensors: [SensorInputDescriptor], depth: [DepthInputDescriptor], camera: [CameraInputDescriptor], audio: [AudioInputDescriptor], location: [LocationInputDescriptor], bluetooth: [BluetoothInputBlockDescriptor]) var results = [Result]() @@ -422,11 +494,12 @@ final class InputElementHandler: ResultElementHandler, LookupElementHandler, Att private let audioHandler = AudioElementHandler() private let locationHandler = LocationElementHandler() private let bluetoothHandler = BluetoothElementHandler() + private let cameraHandler = CameraElementHandler() var childHandlers: [String: ElementHandler] init() { - childHandlers = ["sensor": sensorHandler, "depth": depthHandler, "audio": audioHandler, "location": locationHandler, "bluetooth": bluetoothHandler] + childHandlers = ["sensor": sensorHandler, "depth": depthHandler, "camera": cameraHandler, "audio": audioHandler, "location": locationHandler, "bluetooth": bluetoothHandler] } func endElement(text: String, attributes: AttributeContainer) throws { @@ -434,9 +507,10 @@ final class InputElementHandler: ResultElementHandler, LookupElementHandler, Att let location = locationHandler.results let sensors = sensorHandler.results let depth = depthHandler.results + let camera = cameraHandler.results let bluetooth = bluetoothHandler.results - results.append((sensors, depth, audio, location, bluetooth)) + results.append((sensors, depth, camera, audio, location, bluetooth)) } } diff --git a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/PhyphoxElementHandler.swift b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/PhyphoxElementHandler.swift index 14cbfe56..a868e03a 100644 --- a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/PhyphoxElementHandler.swift +++ b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/PhyphoxElementHandler.swift @@ -38,6 +38,15 @@ private extension ExperimentDepthInput { } } +private extension ExperimentCameraInput { + convenience init(descriptor: CameraInputDescriptor, timeReference: ExperimentTimeReference, buffers: [String: DataBuffer]) { + let zBuffer = descriptor.buffer(for: "z", from: buffers) + let tBuffer = descriptor.buffer(for: "t", from: buffers) + + self.init(timeReference: timeReference, zBuffer: zBuffer, tBuffer: tBuffer, x1: descriptor.x1, x2: descriptor.x2, y1: descriptor.y1, y2: descriptor.y2, smooth: descriptor.smooth, autoExposure: descriptor.autoExposure, exposureAdjustmentLevel: descriptor.exposureAdjustmentLevel, locked: descriptor.locked, feature: descriptor.feature, analysis: descriptor.analysis) + } +} + private extension ExperimentGPSInput { convenience init(descriptor: LocationInputDescriptor, timeReference: ExperimentTimeReference, buffers: [String: DataBuffer]) { let latBuffer = descriptor.buffer(for: "lat", from: buffers) @@ -207,6 +216,13 @@ final class PhyphoxElementHandler: ResultElementHandler, LookupElementHandler { throw ElementHandlerError.message("Depth is only allowed once.") } let depthInput = depthInputs?.first + + let cameraInputs = inputDescriptor?.camera.map {ExperimentCameraInput(descriptor: $0, timeReference: timeReference, buffers: buffers) } + if cameraInputs != nil && depthInputs!.count > 1 { + throw ElementHandlerError.message("Camera is only allowed once.") + } + let cameraInput = cameraInputs?.first + let gpsInputs = inputDescriptor?.location.map { ExperimentGPSInput(descriptor: $0, timeReference: timeReference, buffers: buffers) } ?? [] let audioInputs = try inputDescriptor?.audio.map { try ExperimentAudioInput(descriptor: $0, buffers: buffers) } ?? [] @@ -343,7 +359,7 @@ final class PhyphoxElementHandler: ResultElementHandler, LookupElementHandler { let viewDescriptors = try viewCollectionDescriptors?.map { ExperimentViewCollectionDescriptor(label: $0.label, translation: translations, views: try $0.views.map { try makeViewDescriptor(from: $0, timeReference: timeReference, buffers: buffers, translations: translations) }) } - let experiment = Experiment(title: title, stateTitle: stateTitle, description: description, links: links, category: category, icon: icon, color: color, appleBan: appleBan, isLink: isLink, translation: translations, buffers: buffers, timeReference: timeReference, sensorInputs: sensorInputs, depthInput: depthInput, gpsInputs: gpsInputs, audioInputs: audioInputs, audioOutput: audioOutput, bluetoothDevices: bluetoothDevices, bluetoothInputs: bluetoothInputs, bluetoothOutputs: bluetoothOutputs, networkConnections: networkConnections, viewDescriptors: viewDescriptors, analysis: analysis, export: export) + let experiment = Experiment(title: title, stateTitle: stateTitle, description: description, links: links, category: category, icon: icon, color: color, appleBan: appleBan, isLink: isLink, translation: translations, buffers: buffers, timeReference: timeReference, sensorInputs: sensorInputs, depthInput: depthInput, cameraInput: cameraInput, gpsInputs: gpsInputs, audioInputs: audioInputs, audioOutput: audioOutput, bluetoothDevices: bluetoothDevices, bluetoothInputs: bluetoothInputs, bluetoothOutputs: bluetoothOutputs, networkConnections: networkConnections, viewDescriptors: viewDescriptors, analysis: analysis, export: export) results.append(experiment) } @@ -431,7 +447,12 @@ final class PhyphoxElementHandler: ResultElementHandler, LookupElementHandler { return GraphViewDescriptor(label: descriptor.label, translation: translations, xLabel: descriptor.xLabel, yLabel: descriptor.yLabel, zLabel: descriptor.zLabel, xUnit: descriptor.xUnit, yUnit: descriptor.yUnit, zUnit: descriptor.zUnit, yxUnit: descriptor.yxUnit, timeReference: timeReference, timeOnX: descriptor.timeOnX, timeOnY: descriptor.timeOnY, systemTime: descriptor.systemTime, linearTime: descriptor.linearTime, hideTimeMarkers: descriptor.hideTimeMarkers, xInputBuffers: xBuffers, yInputBuffers: yBuffers, zInputBuffers: zBuffers, logX: descriptor.logX, logY: descriptor.logY, logZ: descriptor.logZ, xPrecision: descriptor.xPrecision, yPrecision: descriptor.yPrecision, zPrecision: descriptor.zPrecision, scaleMinX: descriptor.scaleMinX, scaleMaxX: descriptor.scaleMaxX, scaleMinY: descriptor.scaleMinY, scaleMaxY: descriptor.scaleMaxY, scaleMinZ: descriptor.scaleMinZ, scaleMaxZ: descriptor.scaleMaxZ, minX: descriptor.minX, maxX: descriptor.maxX, minY: descriptor.minY, maxY: descriptor.maxY, minZ: descriptor.minZ, maxZ: descriptor.maxZ, followX: descriptor.followX, aspectRatio: descriptor.aspectRatio, partialUpdate: descriptor.partialUpdate, history: descriptor.history, style: descriptor.style, lineWidth: descriptor.lineWidth, color: descriptor.color, mapWidth: descriptor.mapWidth, colorMap: descriptor.colorMap) case .depthGUI(let descriptor): return DepthGUIViewDescriptor(label: descriptor.label, aspectRatio: descriptor.aspectRatio, translation: translations) + + + case .camera(let descriptor): + return CameraViewDescriptor(label: descriptor.label, aspectRatio: descriptor.aspectRatio, translation: translations) } + } private func makeAudioOutput(from descriptor: AudioOutputDescriptor?, buffers: [String: DataBuffer]) throws -> ExperimentAudioOutput? { diff --git a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewHandlers/CameraViewElementHandler.swift b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewHandlers/CameraViewElementHandler.swift new file mode 100644 index 00000000..245d9e2e --- /dev/null +++ b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewHandlers/CameraViewElementHandler.swift @@ -0,0 +1,40 @@ +// +// CameraViewElementHandler.swift +// phyphox +// +// Created by Gaurav Tripathee on 07.09.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation + +struct CameraViewElementDescriptor { + let label: String + let aspectRatio: CGFloat +} + +final class CameraViewElementHandler: ResultElementHandler, ChildlessElementHandler, ViewComponentElementHandler { + var results = [ViewElementDescriptor]() + + func startElement(attributes: AttributeContainer) throws {} + + private enum Attribute: String, AttributeKey { + case label + case aspectRatio + } + + func endElement(text: String, attributes: AttributeContainer) throws { + let attributes = attributes.attributes(keyedBy: Attribute.self) + + let label = attributes.optionalString(for: .label) ?? "" + + let aspectRatio: CGFloat = try attributes.optionalValue(for: .aspectRatio) ?? 2.5 + + results.append(.camera(CameraViewElementDescriptor(label: label, aspectRatio: aspectRatio))) + } + + func nextResult() throws -> ViewElementDescriptor { + guard !results.isEmpty else { throw ElementHandlerError.missingElement("") } + return results.removeFirst() + } +} diff --git a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewsElementHandler.swift b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewsElementHandler.swift index 04386224..3da40354 100644 --- a/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewsElementHandler.swift +++ b/phyphox-iOS/phyphox/Experiments/Serialization/Handlers/ViewsElementHandler.swift @@ -18,6 +18,7 @@ enum ViewElementDescriptor { case button(ButtonViewElementDescriptor) case graph(GraphViewElementDescriptor) case depthGUI(DepthGUIViewElementDescriptor) + case camera(CameraViewElementDescriptor) } protocol ViewComponentElementHandler: ElementHandler { @@ -39,6 +40,7 @@ private final class ViewElementHandler: ResultElementHandler { private let buttonhandler = ButtonViewElementHandler() private let graphHandler = GraphViewElementHandler() private let depthGUIHandler = DepthGUIViewElementHandler() + private let cameraHandler = CameraViewElementHandler() private var elementOrder = [ViewComponentElementHandler]() @@ -62,6 +64,8 @@ private final class ViewElementHandler: ResultElementHandler { handler = graphHandler case "depth-gui": handler = depthGUIHandler + case "camera-gui": + handler = cameraHandler default: throw ElementHandlerError.unexpectedChildElement(elementName) } @@ -95,6 +99,7 @@ private final class ViewElementHandler: ResultElementHandler { buttonhandler.clear() graphHandler.clear() depthGUIHandler.clear() + cameraHandler.clear() } } diff --git a/phyphox-iOS/phyphox/Experiments/ViewDescriptors/CameraViewDescriptor.swift b/phyphox-iOS/phyphox/Experiments/ViewDescriptors/CameraViewDescriptor.swift new file mode 100644 index 00000000..c282c496 --- /dev/null +++ b/phyphox-iOS/phyphox/Experiments/ViewDescriptors/CameraViewDescriptor.swift @@ -0,0 +1,27 @@ +// +// CameraViewDescriptor.swift +// phyphox +// +// Created by Gaurav Tripathee on 07.09.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation + +struct CameraViewDescriptor: ViewDescriptor, Equatable { + let label: String + let aspectRatio: CGFloat + + let translation: ExperimentTranslationCollection? + + init(label: String, aspectRatio: CGFloat, translation: ExperimentTranslationCollection?) { + self.label = label + self.aspectRatio = aspectRatio + self.translation = translation + } + + func generateViewHTMLWithID(_ id: Int) -> String { + let warningText = localize("remoteCameraWarning").replacingOccurrences(of: "\"", with: "\\\"") + return "
\(localizedLabel)
" + } +} diff --git a/phyphox-iOS/phyphox/Helper/ColorConverterHelper.swift b/phyphox-iOS/phyphox/Helper/ColorConverterHelper.swift index b6b85d2d..4f125fd4 100644 --- a/phyphox-iOS/phyphox/Helper/ColorConverterHelper.swift +++ b/phyphox-iOS/phyphox/Helper/ColorConverterHelper.swift @@ -39,7 +39,29 @@ class ColorConverterHelper { var l = (2.0 - hsv.saturation) * hsv.value / 2.0 let s = l > 0 && l < 1 ? hsv.saturation * hsv.value / (l < 0.5 ? l * 2.0 : 2.0 - l * 2.0) : 0.0 - l = 1.0 - l + + // While flipping HSL lightness (l = 1.0 - l) is a good first approximation, it fails for + // colors where luminance differs massively from lightness. Extreme example: + // ff0000 (yellow) has a lightness of 0.5 and would remain unchanged although it is + // perceived as very bright and has a good contrast to black but almost no contrast to + // white. + // Directly calculating HSL or RGB for a target luminance seems to be tricky according to + // comments in https://stackoverflow.com/a/61761862/8068814, but we do not have to be exact. + // Instead lets flip the luminance and use that for lightness + // (in linear space, though, so need to adjust gamma) + let lum = colorName.linearLuminance + let pivot = 0.21404114 //Math.pow((0.5+0.055)/1.055, 2.4) + var gammaL = pivot * (1 - lum) / (1 - pivot) + if gammaL < 0 { + gammaL = 0.0 + } + l = 1.055 * pow(gammaL, 1.0/2.4) - 0.055; + if l < 0 { + l = 0 + } else if l > 1 { + l = 1 + } + let t = s * (l < 0.5 ? l : 1.0 - l) hsv.value = l + t hsv.saturation = l > 0 ? 2 * t / hsv.value : 0.0 diff --git a/phyphox-iOS/phyphox/Info.plist b/phyphox-iOS/phyphox/Info.plist index 5b80a883..b1910958 100644 --- a/phyphox-iOS/phyphox/Info.plist +++ b/phyphox-iOS/phyphox/Info.plist @@ -49,7 +49,7 @@ CFBundleVersion - 11283 + 13822 LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace diff --git a/phyphox-iOS/phyphox/Settings.bundle/bn.lproj/Root.strings b/phyphox-iOS/phyphox/Settings.bundle/bn.lproj/Root.strings new file mode 100644 index 00000000..ed60b89c Binary files /dev/null and b/phyphox-iOS/phyphox/Settings.bundle/bn.lproj/Root.strings differ diff --git a/phyphox-iOS/phyphox/Settings.bundle/cs.lproj/Root.strings b/phyphox-iOS/phyphox/Settings.bundle/cs.lproj/Root.strings index ed60b89c..59e3181b 100644 Binary files a/phyphox-iOS/phyphox/Settings.bundle/cs.lproj/Root.strings and b/phyphox-iOS/phyphox/Settings.bundle/cs.lproj/Root.strings differ diff --git a/phyphox-iOS/phyphox/Settings.bundle/el.lproj/Root.strings b/phyphox-iOS/phyphox/Settings.bundle/el.lproj/Root.strings index 55920de6..7a1b1f0f 100644 Binary files a/phyphox-iOS/phyphox/Settings.bundle/el.lproj/Root.strings and b/phyphox-iOS/phyphox/Settings.bundle/el.lproj/Root.strings differ diff --git a/phyphox-iOS/phyphox/Settings.bundle/ta.lproj/Root.strings b/phyphox-iOS/phyphox/Settings.bundle/ta.lproj/Root.strings new file mode 100644 index 00000000..ed60b89c Binary files /dev/null and b/phyphox-iOS/phyphox/Settings.bundle/ta.lproj/Root.strings differ diff --git a/phyphox-iOS/phyphox/UI/MainView/BluetoothScanResultsTableViewController.swift b/phyphox-iOS/phyphox/UI/MainView/BluetoothScanResultsTableViewController.swift index dc6d3b41..2dd13508 100644 --- a/phyphox-iOS/phyphox/UI/MainView/BluetoothScanResultsTableViewController.swift +++ b/phyphox-iOS/phyphox/UI/MainView/BluetoothScanResultsTableViewController.swift @@ -34,15 +34,19 @@ class BluetoothScanResultsTableViewController: UITableViewController, ScanResult init(filterByName: String?, filterByUUID: CBUUID?, checkExperiments: Bool, autoConnect: Bool) { ble = BluetoothScan(scanDirectly: true, filterByName: filterByName, filterByUUID: filterByUUID, checkExperiments: checkExperiments, autoConnect: autoConnect) - + signalImages = [] super.init(style: .plain) for i in 0..<5 { - self.showTheSignalImageByAdjustingWithAppMode(i: i) + if #available(iOS 13.0, *) { + signalImages.append(BluetoothScanResultsTableViewController.showTheSignalImageByAdjustingWithAppMode(i: i)) + } else { + signalImages.append(UIImage(named: "cellular_level_\(i)")!) + } } - + ble.scanResultsDelegate = self } @@ -63,7 +67,7 @@ class BluetoothScanResultsTableViewController: UITableViewController, ScanResult override func viewWillDisappear(_ animated: Bool) { ble.stopScan() } - + override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @@ -125,24 +129,21 @@ class BluetoothScanResultsTableViewController: UITableViewController, ScanResult } } - func showTheSignalImageByAdjustingWithAppMode(i: Int){ - if #available(iOS 13.0, *) { - if(SettingBundleHelper.getAppMode() == Utility.LIGHT_MODE){ - signalImages.append((UIImage(named: "bluetooth_signal_\(i)")?.withTintColor(.black, renderingMode: .alwaysOriginal))!) - } else if(SettingBundleHelper.getAppMode() == Utility.DARK_MODE){ - signalImages.append(UIImage(named: "bluetooth_signal_\(i)")!) + @available(iOS 13.0, *) + static func showTheSignalImageByAdjustingWithAppMode(i: Int) -> UIImage{ + if(SettingBundleHelper.getAppMode() == Utility.LIGHT_MODE){ + return (UIImage(named: "cellular_level_\(i)")?.withTintColor(.black, renderingMode: .alwaysOriginal))! + } else if(SettingBundleHelper.getAppMode() == Utility.DARK_MODE){ + return (UIImage(named: "cellular_level_\(i)")?.withTintColor(.white, renderingMode: .alwaysOriginal))! + } else { + if(UIScreen.main.traitCollection.userInterfaceStyle == .light){ + return (UIImage(named: "cellular_level_\(i)")?.withTintColor(.black, renderingMode: .alwaysOriginal))! } else { - if(UIScreen.main.traitCollection.userInterfaceStyle == .light){ - signalImages.append((UIImage(named: "bluetooth_signal_\(i)")?.withTintColor(.black, renderingMode: .alwaysOriginal))!) - } else { - signalImages.append(UIImage(named: "bluetooth_signal_\(i)")!) - } - + return (UIImage(named: "cellular_level_\(i)")?.withTintColor(.white, renderingMode: .alwaysOriginal))! } - } - else { - signalImages.append(UIImage(named: "bluetooth_signal_\(i)")!) + } } - + + } diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentCell.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentCell.swift index 728ba620..d1ec91af 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentCell.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentCell.swift @@ -31,8 +31,7 @@ class ExperimentCell: UICollectionViewCell { return } - let expColor = experiment?.color ?? kHighlightColor - let color = expColor.linearLuminance > 0.1 ? expColor : kTextColor + let color = kTextColor.autoLightColor() optionsButton.setTintColor(color, for: UIControl.State()) optionsButton.setTintColor(color.interpolating(to: UIColor.black, byFraction: 0.5), for: .highlighted) } diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ConnectedBluetoothDeviceViewController.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ConnectedBluetoothDeviceViewController.swift new file mode 100644 index 00000000..be7bec02 --- /dev/null +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ConnectedBluetoothDeviceViewController.swift @@ -0,0 +1,173 @@ +// +// ConnectedBluetoothDeviceViewController.swift +// phyphox +// +// Created by Gaurav Tripathee on 10.03.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation + +class ConnectedBluetoothDevicesViewController: UICollectionView, UICollectionViewDataSource { + + private var data: [ConnectedDevicesDataModel] = [ConnectedDevicesDataModel]() + let cellIdentifier: String = "ConnectedBleDeviceCell" + + init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout, data: [ConnectedDevicesDataModel]) { + super.init(frame: frame, collectionViewLayout: layout) + self.register(ConnectedBleDeviceCell.self, forCellWithReuseIdentifier: cellIdentifier) + self.dataSource = self + self.data = data + layout.collectionView?.backgroundColor = UIColor(named: "backgroundDark") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return data.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier , for: indexPath) as? ConnectedBleDeviceCell else { + return UICollectionViewCell() + } + + cell.contentView.backgroundColor = UIColor(named: "backgroundDark") + + let dataModel: ConnectedDevicesDataModel = ConnectedDevicesDataModel( + deviceIdentifier:data[indexPath.row].getDeviceIdentifier(), + signalStrength:data[indexPath.row].getSignalStrength(), + batteryLabel: data[indexPath.row].getBatteryLabel(), + deviceName: data[indexPath.row].getDeviceName()) + + cell.configure(model: dataModel) + + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + // handle cell selection + } + +} + +class ConnectedBleDeviceCell: UICollectionViewCell { + + let batteryImageView = UIImageView() + let signalImageView = UIImageView() + let deviceLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + + deviceLabel.font = UIFont.systemFont(ofSize: 14) + deviceLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(deviceLabel) + NSLayoutConstraint.activate([ + deviceLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + deviceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + deviceLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), + ]) + + batteryImageView.contentMode = .scaleAspectFit + + batteryImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(batteryImageView) + NSLayoutConstraint.activate([ + batteryImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + batteryImageView.leadingAnchor.constraint(equalTo: deviceLabel.trailingAnchor, constant: 8), + + batteryImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + + signalImageView.contentMode = .scaleAspectFit + signalImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(signalImageView) + NSLayoutConstraint.activate([ + signalImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + signalImageView.leadingAnchor.constraint(equalTo: batteryImageView.trailingAnchor, constant: 8), + signalImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + signalImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @available(iOS 13.0, *) + func getSignal(rssi: Int) -> UIImage{ + + if rssi > -35 { + return UIImage(named: "cellular_level_4")! + } else if rssi > -55 { + return UIImage(named: "cellular_level_3")! + } else if rssi > -70 { + return UIImage(named: "cellular_level_2")! + } else if rssi > -90 { + return UIImage(named: "cellular_level_1")! + } else { + return UIImage(named: "cellular_level_0")! + } + + + } + + @available(iOS 13.0, *) + func getBatteryLevel(level: Int) -> UIImage{ + let config = UIImage.SymbolConfiguration( + pointSize: 25, weight: .medium, scale: .default) + + if level == -1 { + return UIImage() + + } else if level < 5 { + return UIImage(systemName: "battery.0", withConfiguration: config)! + } + else if level < 25 { + return UIImage(systemName: "battery.25", withConfiguration: config)! + } else if level < 50 { + return UIImage(systemName: "battery.50", withConfiguration: config)! + } else if level < 75 { + return UIImage(systemName: "battery.75", withConfiguration: config)! + } else if level < 100 { + return UIImage(systemName: "battery.100", withConfiguration: config)! + } else{ + return UIImage(systemName: "battery.100", withConfiguration: config)! + } + + } + + + @available(iOS 13.0, *) + func getImageAsDeviceMode(image: UIImage) -> UIImage{ + if(SettingBundleHelper.getAppMode() == Utility.LIGHT_MODE){ + return image.withTintColor(.black, renderingMode: .alwaysOriginal) + } else if(SettingBundleHelper.getAppMode() == Utility.DARK_MODE){ + return image.withTintColor(UIColor(white: 1.0, alpha: 1.0), renderingMode: .alwaysOriginal) + } else { + if(UIScreen.main.traitCollection.userInterfaceStyle == .light){ + return image.withTintColor(.black, renderingMode: .alwaysOriginal) + } else { + return image.withTintColor(UIColor(white: 1.0, alpha: 1.0), renderingMode: .alwaysOriginal) + } + } + } + + func configure(model: ConnectedDevicesDataModel){ + deviceLabel.text = model.getDeviceName() + if #available(iOS 13.0, *) { + signalImageView.image = getImageAsDeviceMode(image: getSignal(rssi: model.getSignalStrength())) + batteryImageView.image = getImageAsDeviceMode(image: getBatteryLevel(level: model.getBatteryLabel())) + } else { + // Fallback on earlier versions + } + } + +} diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ExperimentPageViewController.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ExperimentPageViewController.swift index 69c15dd2..e98185ff 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ExperimentPageViewController.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ExperimentPageViewController.swift @@ -7,7 +7,7 @@ // import Foundation -import GCDWebServers +import GCDWebServer import SwiftUI protocol ExportDelegate { @@ -17,7 +17,8 @@ protocol ExportDelegate { protocol StopExperimentDelegate { func stopExperiment() } -final class ExperimentPageViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIPopoverPresentationControllerDelegate, ExperimentWebServerDelegate, ExportDelegate, StopExperimentDelegate, BluetoothScanDialogDismissedDelegate, NetworkScanDialogDismissedDelegate, NetworkConnectionDataPolicyInfoDelegate { + +final class ExperimentPageViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIPopoverPresentationControllerDelegate, ExperimentWebServerDelegate, ExportDelegate, StopExperimentDelegate, BluetoothScanDialogDismissedDelegate, NetworkScanDialogDismissedDelegate, NetworkConnectionDataPolicyInfoDelegate, UpdateConnectedDeviceDelegate{ var actionItem: UIBarButtonItem? var playItem: UIBarButtonItem? @@ -42,6 +43,10 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController private let viewModules: [[UIView]] + let flowLayout = UICollectionViewFlowLayout() + + var numOfConnectedDevices = 0 + var timerRunning: Bool { return experimentRunTimer != nil } @@ -121,7 +126,6 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController if let descriptors = experiment.viewDescriptors { for collection in descriptors { let m = ExperimentViewModuleFactory.createViews(collection) - modules.append(m) experimentViewControllers.append(ExperimentViewController(modules: m)) @@ -148,6 +152,16 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController } } + + flowLayout.scrollDirection = .vertical + flowLayout.minimumLineSpacing = 10 + flowLayout.minimumInteritemSpacing = 10 + + flowLayout.itemSize = CGSize(width: self.view.frame.width, height: 40) + + ExperimentBluetoothDevice.updateDelegate = self + + self.navigationItem.title = experiment.displayTitle let backButton = UIBarButtonItem(title: "‹", style: .plain, target: self, action: #selector(leaveExperiment)) @@ -189,6 +203,7 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + print("view will apprear") if isMovingToParent { experiment.willBecomeActive { DispatchQueue.main.async { @@ -265,28 +280,47 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController var pageViewControlerRect = CGRect(x: 0, y: offsetTop, width: self.view.frame.width, height: self.view.frame.height-offsetTop) + var adjustableHeightS1 = 0.0 + var adjustableHeightS2 = 0.0 + if( numOfConnectedDevices == 1){ + adjustableHeightS1 = 30.0 + adjustableHeightS2 = 20.0 + } else if(numOfConnectedDevices > 1){ + adjustableHeightS1 = 82.0 + adjustableHeightS2 = 72.0 + } + if let label = self.serverLabel, let labelBackground = self.serverLabelBackground { let s = label.sizeThatFits(CGSize(width: offsetFrame.width, height: 300)) - pageViewControlerRect = CGRect(origin: pageViewControlerRect.origin, size: CGSize(width: pageViewControlerRect.width, height: pageViewControlerRect.height-s.height-offsetBottom)) + pageViewControlerRect = CGRect(origin: pageViewControlerRect.origin, size: CGSize(width: pageViewControlerRect.width, height: pageViewControlerRect.height-s.height-offsetBottom - adjustableHeightS1)) - let labelBackgroundFrame = CGRect(x: 0, y: self.view.frame.height - s.height - offsetBottom, width: pageViewControlerRect.width, height: s.height + offsetBottom) - let labelFrame = CGRect(x: offsetFrame.minX, y: self.view.frame.height - s.height - offsetBottom, width: offsetFrame.width, height: s.height) + let labelBackgroundFrame = CGRect(x: 0, y: self.view.frame.height - s.height - offsetBottom - adjustableHeightS1 , width: pageViewControlerRect.width, height: s.height + offsetBottom - adjustableHeightS2) + let labelFrame = CGRect(x: offsetFrame.minX, y: self.view.frame.height - s.height - offsetBottom - adjustableHeightS2 , width: offsetFrame.width, height: s.height ) label.frame = labelFrame labelBackground.frame = labelBackgroundFrame label.autoresizingMask = [.flexibleTopMargin, .flexibleWidth] labelBackground.autoresizingMask = [.flexibleTopMargin, .flexibleWidth] } + if self.serverQRIcon != nil { + NSLayoutConstraint.activate([ + self.serverQRIcon!.trailingAnchor.constraint(equalTo: self.serverLabelBackground!.trailingAnchor, constant: -15.0), + self.serverQRIcon!.bottomAnchor.constraint(equalTo: self.serverLabelBackground!.bottomAnchor, constant: -20.0 ) + ]) + } + self.pageViewControler.view.frame = pageViewControlerRect } override func viewDidLoad() { super.viewDidLoad() + print("View did load") + self.automaticallyAdjustsScrollViewInsets = false self.edgesForExtendedLayout = UIRectEdge() - refreshTheAdjustedGraphColorForLightMode() + refreshAppTheme() actionItem = UIBarButtonItem(image: generateDots(20.0), landscapeImagePhone: generateDots(15.0), style: .plain, target: self, action: #selector(action(_:))) actionItem?.accessibilityLabel = localize("actions") @@ -340,6 +374,8 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController pageViewControler.setViewControllers([experimentViewControllers[0]], direction: .forward, animated: false, completion: nil) pageViewControler.view.autoresizingMask = [.flexibleHeight, .flexibleWidth] + + updateLayout() self.addChild(pageViewControler) @@ -349,16 +385,18 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController updateSelectedViewCollection() + } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateSelectedViewCollection() - refreshTheAdjustedGraphColorForLightMode() + refreshAppTheme() if (experiment.viewDescriptors!.count > 1) { updateSegControlDesign() tabBar!.backgroundColor = SettingBundleHelper.getLightBackgroundColorWhenDarkModeNotSupported() } + } class NetworkServiceRequestCallbackWrapper: NetworkServiceRequestCallback { @@ -475,8 +513,22 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController session.attachDelegate(delegate: depthGUI) depthGUI.depthGUISelectionDelegate = session } + + if let cameraGUI = view as? ExperimentCameraUIView { + guard let session = experiment.cameraInput?.session as? ExperimentCameraInputSession else { + continue + } + session.initializeCameraModel() + print("viewDidAppear") + session.attachDelegate(delegate: cameraGUI as! CameraGUIDelegate) + cameraGUI.cameraSelectionDelegate = session.cameraModel as? any CameraSelectionDelegate + cameraGUI.cameraViewDelegete = session.cameraModel as? any CameraViewDelegate + print("experimentViewController") + } + } } + } if let networkConnection = experiment.networkConnections.first { var sensorList: [String] = [] @@ -498,10 +550,14 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController if let session = experiment.depthInput?.session as? ExperimentDepthInputSession { session.stopSession() } + + if let camSession = experiment.cameraInput?.session as? ExperimentCameraInputSession { + camSession.endSession() + } } disconnectFromBluetoothDevices() disconnectFromNetworkDevices() - + print("viewDidDisappear") if isMovingFromParent { tearDownWebServer() @@ -631,11 +687,20 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController self.serverLabel!.inputAccessoryView = UIView() self.serverQRIcon = UIButton(type: .system) - let image = UIImage(named: "new_experiment_qr")!.resize(size: CGSize(width: 30, height: 30)) + + var image = UIImage(named: "new_experiment_qr")!.resize(size: CGSize(width: 30, height: 30)) + if #available(iOS 13.0, *) { + let config = UIImage.SymbolConfiguration( + pointSize: 25, weight: .medium, scale: .default) + + image = UIImage(systemName: "info.circle.fill", withConfiguration: config)! + } else { + // Fallback on earlier versions + } + self.serverQRIcon?.setImage(image, for: .normal) self.serverQRIcon?.addTarget(self, action: #selector(showQr), for: .touchUpInside) self.serverQRIcon?.imageView?.contentMode = .scaleAspectFit - self.serverLabelBackground = UIView() self.serverLabelBackground!.backgroundColor = UIColor(named: "lightBackgroundColor") ?? kLightBackgroundColor @@ -645,15 +710,12 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController // set view1 constraints self.serverQRIcon!.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - self.serverQRIcon!.trailingAnchor.constraint(equalTo: self.serverLabelBackground!.trailingAnchor, constant: -20.0), - self.serverQRIcon!.bottomAnchor.constraint(equalTo: self.serverLabelBackground!.bottomAnchor, constant: -25.0) - ]) updateLayout() } } + @objc func showQr(){ let data = remoteUrl.data(using: .utf8) @@ -1430,7 +1492,7 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController } } - func refreshTheAdjustedGraphColorForLightMode(){ + func refreshAppTheme(){ if #available(iOS 12.0, *) { if(SettingBundleHelper.getAppMode() == Utility.LIGHT_MODE || UIScreen.main.traitCollection.userInterfaceStyle == .light){ @@ -1439,11 +1501,32 @@ final class ExperimentPageViewController: UIViewController, UIPageViewController } else { // Fallback on earlier versions } + } else if(SettingBundleHelper.getAppMode() == Utility.DARK_MODE || + UIScreen.main.traitCollection.userInterfaceStyle == .dark){ + if #available(iOS 13.0, *) { + view.overrideUserInterfaceStyle = .dark + } else { + // Fallback on earlier versions + } } } else { // Fallback on earlier versions } } + + func showUpdatedConnectedDevices(connectedDevice: [ConnectedDevicesDataModel]) { + numOfConnectedDevices = connectedDevice.count + var adjustedHeight = 48.0 + if( numOfConnectedDevices == 0){ + return + } else if(numOfConnectedDevices > 1){ + adjustedHeight = 100.0 + } + + let customCollectionView = ConnectedBluetoothDevicesViewController(frame: CGRect(x: 0, y: self.view.frame.height - adjustedHeight, width: self.view.frame.width, height: adjustedHeight ), collectionViewLayout: flowLayout, data: connectedDevice) + view.addSubview(customCollectionView) + + } } extension ExperimentPageViewController: ExperimentAnalysisDelegate { diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ApplyZoomDialog.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ApplyZoomDialog.swift index 6a0171ca..0bcb3a24 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ApplyZoomDialog.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ApplyZoomDialog.swift @@ -229,6 +229,7 @@ class ApplyZoomDialog: UIViewController, UITableViewDataSource, UITableViewDeleg let titleView = UILabel() titleView.translatesAutoresizingMaskIntoConstraints = false titleView.text = localize("applyZoomTitle") + titleView.textColor = UIColor(named: "textColor") titleView.font = UIFont.preferredFont(forTextStyle: .headline) dialogView.addSubview(titleView) @@ -244,6 +245,7 @@ class ApplyZoomDialog: UIViewController, UITableViewDataSource, UITableViewDeleg let explView = UILabel() explView.translatesAutoresizingMaskIntoConstraints = false explView.text = localize("applyZoomExplanation") + explView.textColor = UIColor(named: "textColor") explView.lineBreakMode = .byWordWrapping explView.numberOfLines = 0 scrollContentView.addSubview(explView) @@ -266,6 +268,7 @@ class ApplyZoomDialog: UIViewController, UITableViewDataSource, UITableViewDeleg let advancedSwitchLabel = UILabel() advancedSwitchLabel.translatesAutoresizingMaskIntoConstraints = false advancedSwitchLabel.text = localize("applyZoomAdvanced") + advancedSwitchLabel.textColor = UIColor(named: "textColor") dialogView.addSubview(advancedSwitchLabel) let okButton = UIButton() diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ExperimentGraphView.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ExperimentGraphView.swift index 826df7ea..5736c035 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ExperimentGraphView.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/ExperimentGraphView.swift @@ -287,7 +287,7 @@ final class ExperimentGraphView: UIView, DynamicViewModule, ResizableViewModule, } if #available(iOS 13.0, *) { let config = UIImage.SymbolConfiguration( - pointSize: 25, weight: .medium, scale: .default) + pointSize: 25, weight: .regular, scale: .default) unfoldLessImageView = UIImageView(image: UIImage(systemName: "arrow.down.right.and.arrow.up.left", withConfiguration: config)) unfoldMoreImageView = UIImageView(image: UIImage(systemName: "arrow.up.left.and.arrow.down.right", withConfiguration: config)) diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/Graph/Grid/GraphGridView.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/Graph/Grid/GraphGridView.swift index 6773b646..77b466c9 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/Graph/Grid/GraphGridView.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Dynamic/Graph/Grid/GraphGridView.swift @@ -19,6 +19,16 @@ final class GraphGridView: UIView { var isZScale: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + + borderView.layer.borderColor = getAdjustedBorderColor() + borderView.layer.borderWidth = SettingBundleHelper.getGraphSettingWidth()/UIScreen.main.scale + + addSubview(borderView) + } + var gridInset: CGPoint = .zero { didSet { setNeedsLayout() @@ -358,11 +368,26 @@ final class GraphGridView: UIView { self.layoutSubviews() } + + private func getAdjustedBorderColor() -> CGColor{ + var borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor + if(SettingBundleHelper.getAppMode() == Utility.LIGHT_MODE){ + borderColor = UIColor(white: 0.0, alpha: 1.0).cgColor + } else if(SettingBundleHelper.getAppMode() == Utility.SYSTEM_MODE){ + if #available(iOS 12.0, *) { + if(UIScreen.main.traitCollection.userInterfaceStyle == .light) { + borderColor = UIColor(white: 0.0, alpha: 1.0).cgColor + } + } + } + return borderColor + } + @objc func reloadBorderWidth(){ borderView.layer.borderWidth = SettingBundleHelper.getGraphSettingWidth()/UIScreen.main.scale self.layoutSubviews() self.setNeedsDisplay() } - + } diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExperimentViewModuleFactory.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExperimentViewModuleFactory.swift index f2111310..b0a9f9d0 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExperimentViewModuleFactory.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExperimentViewModuleFactory.swift @@ -49,6 +49,16 @@ final class ExperimentViewModuleFactory { print("DepthGUI not supported below iOS 14") //Should not happen as the depth input is marked as unavailable below iOS 14 } + } else if let descriptor = descriptor as? CameraViewDescriptor { + if #available(iOS 14.0, *) { + // TODO need to pass descriptor in view argument. + + views.append(ExperimentCameraUIView(descriptor: descriptor)) + //views.append(UIHostingController(rootView: PhyphoxCameraView()).view) + } else { + // Fallback on earlier versions + } + } else { print("Error! Invalid view descriptor: \(descriptor)") diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExtensionExperimentViewModuleFactory.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExtensionExperimentViewModuleFactory.swift new file mode 100644 index 00000000..2bdee6b4 --- /dev/null +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/ExtensionExperimentViewModuleFactory.swift @@ -0,0 +1,42 @@ +// +// ExtensionExperimentViewModuleFactory.swift +// phyphox +// +// Created by Gaurav Tripathee on 18.03.24. +// Copyright © 2024 RWTH Aachen. All rights reserved. +// + +import Foundation + +import SwiftUI + +extension ExperimentViewModuleFactory { + + @available(iOS 13.0, *) + class func createSwiftUiViews(_ viewDescriptor: ExperimentViewCollectionDescriptor) -> [UIView] { + + var views: [UIView] = [] + + /** + for descriptor in viewDescriptor.views { + if let descriptor = descriptor as? CameraViewDescriptor { + if #available(iOS 14.0, *) { + // TODO need to pass descriptor in view argument. + let hostingController = UIHostingController(rootView: PhyphoxCameraView()) + views.append(hostingController.view) + //views.append(UIHostingController(rootView: PhyphoxCameraView()).view) + } else { + // Fallback on earlier versions + } + + } else { + print("Error! Invalid view descriptor: \(descriptor)") + } + } + */ + + return views.compactMap { $0 } + + } + +} diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/DepthGUI/ExperimentDepthGUIRenderer.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/DepthGUI/ExperimentDepthGUIRenderer.swift index 55f01833..aaa61f59 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/DepthGUI/ExperimentDepthGUIRenderer.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/DepthGUI/ExperimentDepthGUIRenderer.swift @@ -265,7 +265,7 @@ class ExperimentDepthGUIRenderer { let p2 = CGPoint(x: CGFloat(selectionState.x2), y: CGFloat(selectionState.y2)).applying(displayToCameraTransform.inverted()) var scaledSelectionState = SelectionStruct(x1: Float(min(p1.x, p2.x)*viewportSize.width), x2: Float(max(p1.x, p2.x)*viewportSize.width), y1: Float(min(p1.y, p2.y)*viewportSize.height), y2: Float(max(p1.y, p2.y)*viewportSize.height), editable: selectionState.editable) renderEncoder.setFragmentBytes(&scaledSelectionState, length: MemoryLayout.stride, index: 2) - + // Setup plane vertex buffers. renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 0) renderEncoder.setVertexBuffer(imagePlaneVertexBuffer, offset: 0, index: 1) diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentCameraGUIView.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentCameraGUIView.swift new file mode 100644 index 00000000..4f0ef426 --- /dev/null +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentCameraGUIView.swift @@ -0,0 +1,204 @@ +// +// ExperimentCameraView.swift +// phyphox +// +// Created by Gaurav Tripathee on 07.09.23. +// Copyright © 2023 RWTH Aachen. All rights reserved. +// + +import Foundation +import AVFoundation +import SwiftUI + +protocol CameraGUIDelegate { + func updateFrame(captureSession: AVCaptureSession) + func updateResolution(resolution: CGSize) +} + +protocol CameraGUISelectionDelegate { + var x1: Float { get set } + var x2: Float { get set } + var y1: Float { get set } + var y2: Float { get set } + var frontCamera: Bool { get set } +} + + + +@available(iOS 13.0, *) +final class ExperimentCameraGUIView: UIView { + + var videoPreviewLayer: AVCaptureVideoPreviewLayer? + + var resolution: CGSize? + + var layoutDelegate: ModuleExclusiveLayoutDelegate? = nil + var resizableState: ResizableViewModuleState = .normal + + var showMoreButton: UIButton = UIButton() + + var showLessButton: UIButton = UIButton() + + let videoPreviewUIView = UIView() + + private let label = UILabel() + + required init() { + + super.init(frame: .zero) + + setUpCameraViews() + + addSubview(showMoreButton) + addSubview(showLessButton) + + //horizontalCameraSettingsStackView() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + return size + } + + override func layoutSubviews() { + super.layoutSubviews() + + print("layoutSubviews") + var cameraWidth : CGFloat + var cameraHeight : CGFloat + var previewXPosition: CGFloat + + + switch resizableState { + case .normal: + cameraWidth = (UIScreen.main.bounds.width - 20) / 2 + cameraHeight = UIScreen.main.bounds.height / 3 + previewXPosition = (UIScreen.main.bounds.width / 2) - ((videoPreviewLayer?.frame.width ?? 0)/2) + case .exclusive: + cameraWidth = UIScreen.main.bounds.width - 20 + cameraHeight = UIScreen.main.bounds.height / 2 + previewXPosition = 10 + case .hidden: + cameraWidth = 0 + cameraHeight = 0 + previewXPosition = 10 + } + + if(videoPreviewLayer != nil){ + print("updateFrame: layoutSubviews") + videoPreviewLayer?.frame = CGRect(x: previewXPosition, y: 40, width: cameraWidth, height: cameraHeight) + videoPreviewLayer?.contents = + videoPreviewLayer?.videoGravity = .resizeAspectFill + videoPreviewUIView.layer.addSublayer(videoPreviewLayer!) + addSubview(videoPreviewUIView) + + } + + } + + func updateFrame(captureSession: AVCaptureSession) { + //videoPreviewLayer = ScannerOverlayPreviewLayer(session: captureSession) + videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + layoutSubviews() + + } + + func updateResolution(resolution: CGSize) { + self.resolution = resolution + setNeedsLayout() + } + + func setUpCameraViews() { + + let unfoldRect = CGRectMake(5, 5, 40, 40) + + if #available(iOS 13.0, *) { + showMoreButton.setImage(UIImage(systemName: "arrow.up.left.and.arrow.down.right"), for: .normal) + } else { + showMoreButton.setImage(UIImage(named: "unfold_more"), for: .normal) + } + showMoreButton.frame = unfoldRect + showMoreButton.contentMode = .scaleAspectFill + showMoreButton.isHidden = false + showMoreButton.addTarget(self, action: #selector(maximizeCamera), for: .touchUpInside) + + if #available(iOS 13.0, *) { + showLessButton.setImage(UIImage(systemName: "arrow.down.right.and.arrow.up.left"), for: .normal) + } else { + showLessButton.setImage(UIImage(named: "unfold_less"), for: .normal) + } + showLessButton.frame = unfoldRect + showLessButton.isHidden = true + showLessButton.addTarget(self, action: #selector(minimizeCamera), for: .touchUpInside) + + + } + + @objc private func maximizeCamera() { + print("maximizeCamera") + + showLessButton.isHidden = false + showMoreButton.isHidden = true + + resizableState = .exclusive + layoutSubviews() + } + + @objc private func minimizeCamera() { + print("minimizeCamera") + + showLessButton.isHidden = true + showMoreButton.isHidden = false + + resizableState = .normal + layoutSubviews() + } + + private func horizontalCameraSettingsStackView(){ + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .fill + stackView.distribution = .fillEqually + stackView.spacing = 10 + stackView.translatesAutoresizingMaskIntoConstraints = false + + // Create buttons + for i in 1...5 { + let button = UIButton() + button.setTitle("Button \(i)", for: .normal) + button.setTitleColor( UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0), for: .normal) + button.setImage(UIImage(named: "new_experiment_simple"), for: .normal) + button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(button) + } + + addSubview(stackView) + + NSLayoutConstraint.activate([ + // Video preview view constraints + videoPreviewUIView.topAnchor.constraint(equalTo: topAnchor), + videoPreviewUIView.leadingAnchor.constraint(equalTo: leadingAnchor), + videoPreviewUIView.trailingAnchor.constraint(equalTo: trailingAnchor), + videoPreviewUIView.bottomAnchor.constraint(equalTo: bottomAnchor), + + // Button 1 constraints + stackView.topAnchor.constraint(equalTo: videoPreviewUIView.bottomAnchor, constant: 20), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), + + ]) + + + } + + + @objc func buttonTapped(_ sender: UIButton) { + print("Button tapped: \(sender.titleLabel?.text ?? "")") + } + + +} diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentDepthGUIView.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentDepthGUIView.swift index 094bce66..9f4ea391 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentDepthGUIView.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/ViewModules/Static/ExperimentDepthGUIView.swift @@ -39,6 +39,7 @@ final class ExperimentDepthGUIView: UIView, DescriptorBoundViewModule, Resizable var depthGUISelectionDelegate: DepthGUISelectionDelegate? func updateResolution(resolution: CGSize) { + print("updateResolution resolution ", resolution) self.resolution = resolution setNeedsLayout() } @@ -73,7 +74,7 @@ final class ExperimentDepthGUIView: UIView, DescriptorBoundViewModule, Resizable private let buttonPadding: CGFloat = 20.0 - private let label = UILabel() + private let label = UILabel() private let arView = MTKView() let renderer: ExperimentDepthGUIRenderer private let aggregationBtn = UIButton() @@ -89,7 +90,7 @@ final class ExperimentDepthGUIView: UIView, DescriptorBoundViewModule, Resizable arView.device = MTLCreateSystemDefaultDevice() arView.backgroundColor = UIColor.clear - + renderer = ExperimentDepthGUIRenderer(metalDevice: arView.device!, renderDestination: arView) aggregationBtn.backgroundColor = UIColor(named: "lightBackgroundColor") @@ -191,6 +192,9 @@ final class ExperimentDepthGUIView: UIView, DescriptorBoundViewModule, Resizable w = frame.width - 2*sideMargins h = frame.height - 2*spacing - s.height - buttonH - button2H } + + print("updateResolution w ", w) + print("updateResolution h ", h) arView.frame = CGRect(x: (frame.width - w)/2, y: 2*spacing + s.height, width: w, height: h) if resizableState == .exclusive { aggregationBtn.frame = CGRect(x: (frame.width - buttonS.width)/2, y: 2*spacing + s.height + h + 2*spacing, width: buttonS.width, height: buttonS.height) diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/WebServer/ExperimentWebServer.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/WebServer/ExperimentWebServer.swift index 9c15028e..2865cf37 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentView/WebServer/ExperimentWebServer.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentView/WebServer/ExperimentWebServer.swift @@ -7,7 +7,7 @@ // import Foundation -import GCDWebServers +import GCDWebServer protocol ExperimentWebServerDelegate: AnyObject { var timerRunning: Bool { get } diff --git a/phyphox-iOS/phyphox/UI/MainView/ExperimentsCollectionViewController.swift b/phyphox-iOS/phyphox/UI/MainView/ExperimentsCollectionViewController.swift index 7c530735..511bf97f 100644 --- a/phyphox-iOS/phyphox/UI/MainView/ExperimentsCollectionViewController.swift +++ b/phyphox-iOS/phyphox/UI/MainView/ExperimentsCollectionViewController.swift @@ -370,9 +370,8 @@ final class ExperimentsCollectionViewController: CollectionViewController, Exper private func showStateTitleEditForExperiment(_ experiment: Experiment, button: UIButton, oldTitle: String) { - - UIAlertController.PhyphoxUIAlertBuilder() - .title(title: localize("rename")) + let alertBuilder = UIAlertController.PhyphoxUIAlertBuilder() + alertBuilder.title(title: localize("rename")) .message(message: "") .preferredStyle(style: .alert) .addTextField(configHandler: {(textfield: UITextField!) -> Void in @@ -381,7 +380,7 @@ final class ExperimentsCollectionViewController: CollectionViewController, Exper }) .addActionWithTitle(localize("rename"), style: .default, handler: { [unowned self] action in do { - let textField = UIAlertController.PhyphoxUIAlertBuilder().getTextFieldValue() + let textField = alertBuilder.getTextFieldValue() if let newTitle = textField.text, newTitle.replacingOccurrences(of: " ", with: "") != "" { try ExperimentManager.shared.renameExperiment(experiment, newTitle: newTitle) diff --git a/phyphox-iOS/phyphox/bn.lproj/InfoPlist.strings b/phyphox-iOS/phyphox/bn.lproj/InfoPlist.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/phyphox-iOS/phyphox/bn.lproj/InfoPlist.strings @@ -0,0 +1 @@ + diff --git a/phyphox-iOS/phyphox/bn.lproj/Localizable.strings b/phyphox-iOS/phyphox/bn.lproj/Localizable.strings new file mode 100644 index 00000000..6958bb6f --- /dev/null +++ b/phyphox-iOS/phyphox/bn.lproj/Localizable.strings @@ -0,0 +1,76 @@ + +"app_name" = "ফিফক্স"; +"action_settings" = "সেটিংস"; +"title_activity_experiment" = "ফিফক্স"; +"newExperimentSimple" = "সহজ পরীক্ষা যোগ করুন"; +"newExperimentPhyphoxInfo" = "আরো উন্নতমানের পরীক্ষা বা ডাটা বিশ্লেষণ যোগ করতে হলে phyphox.org সাইটে যান। আপনার সহকর্মী, বন্ধু আর ছাত্রদের সাথে ভাগ করে নিন।"; +"newExperimentInputTitle" = "শীর্ষক"; +"newExperimentInputBufferSize" = "বাফারের আকার (যতগুলি রাশি সংগ্রহ করা হবে তার সংখ্যা)"; +"newExperimentInputRate" = "সেন্সরের হার (rate) (হার্ৎসে, 0 = যতো বেশি সম্ভব"; +"newExperimentInputSensors" = "সক্রিয় সেন্সর"; +"warning" = "সাবধান"; +"donotshowagain" = "পুনরায় দেখাবেন না"; +"damageWarning" = "পরীক্ষা করার সময় যাতে আপনার ফোনের কোনো রকম ক্ষতি না হয় সেদিকে দৃষ্টি দেবেন। বিশেষ করে খেয়াল রাখবেন যেন ফোন কোনো শক্ত জায়গায় না পড়ে বা অতিরিক্ত প্রাবল্যের চৌম্বক ক্ষেত্রের সংস্পর্শে না আসে। আপনার ফোনের কোনো ক্ষতির জন্য ফিফক্স প্রণেতারা দায়ী থাকবেন না।"; +"info" = "তথ্য"; +"credits" = "স্বীকৃতি"; +"creditsNames" = "মূল ধারণা এবং প্রস্তুতি :\nডাঃ. সেবাস্টিয়ান স্ট্যাকস\nমূল ধারণা:\nপ্রফেসর ক্রিস্টোফ স্ট্যাম্পফার\nপ্রোগ্রামিং:\nগৌরব ত্রিপাঠী\nডমিনিক ডরসেল\nজোনাস গেসনার\nক্যামিলা লুমারঝেইম"; +"help" = "তথ্য এবং সহায়তা"; +"experimentsPhyphoxOrg" = "পরীক্ষার ধারণা ও প্রণালী"; +"experimentsPhyphoxOrgURL" = "http://phyphox.org/experiments"; +"faqPhyphoxOrg" = "প্রায়শই জিজ্ঞাসা করা প্রশ্ন"; +"faqPhyphoxOrgURL" = "http://phyphox.org/faq"; +"remotePhyphoxOrg" = "রিমোট কন্ট্রোল সহায়তা"; +"remotePhyphoxOrgURL" = "http://phyphox.org/remote-control"; +"deviceInfo" = "ডিভাইসের বিবরণ"; +"copyToClipboard" = "ক্লিপবোর্ডে কপি করুন"; +"deviceInfoCopied" = "ডিভাইস সম্বন্ধে তথ্য আপনার ক্লিপবোর্ডে কপি করা হয়েছে।"; +"graph_tools_pan_and_zoom" = "আঙুল দিয়ে জুম"; +"graph_tools_pick" = "ডাটা নেওয়া"; +"graph_tools_more" = "আরো টুলস"; +"graph_tools_system_time" = "সিস্টেমের সময়ে পাল্টান"; +"graph_tools_reset" = "জুম রিসেট করুন"; +"graph_tools_follow" = "নতুন ডাটা ফলো করুন"; +"graph_tools_linear_fit" = "রৈখিক ফিট"; +"graph_tools_export" = "এই ডেটাসেট রপ্তানি করুন"; +"graph_tools_log_x" = "লগারিদমিক x অক্ষ"; +"graph_tools_log_y" = "লগারিদমিক y অক্ষ"; +"graph_point_label" = "বিন্দু"; +"graph_slope_label" = "নতি"; +"graph_difference_label" = "তফাত"; +"graph_fit_label" = "রৈখিক ফিট: y = a x + b"; +"applyZoomTitle" = "জুম করুন"; +"applyZoomExplanation" = "আপনি ইন্টার‍্যাক্টিভ মোড ছেড়ে বেরোচ্ছেন। আপনি চাইলে বর্তমান জুম স্তর বজায় রাখতে পারেন।"; +"applyZoomAdvanced" = "উন্নত (Advanced) বিকল্প"; +"applyZoomApply" = "প্রয়োগ করুন…"; +"applyZoomReset" = "ডিফল্টে রিসেট"; +"applyZoomKeep" = "পছন্দ হিসেবে রাখুন"; +"applyZoomFollow" = "নতুন ডাটা রাখুন আর তাকে অনুসরণ করুন"; +"applyZoomThis" = "কেবল এই গ্রাফ"; +"applyZoomSameVariable" = "অন্য গ্রাফেওঃ একই চলরাশি (variable)"; +"applyZoomSameUnit" = "অন্য গ্রাফেঃ একই একক"; +"applyZoomSameAxis" = "অন্য গ্রাফেঃ একই অক্ষ"; +"newExperimentBluetooth" = "ব্লু-টুথ যন্ত্রের জন্য পরীক্ষা যোগ করুন"; +"newExperimentBluetoothErrorTitle" = "ব্লু-টুথ স্ক্যানে ত্রুটি"; +"newExperimentBluetoothLoadFromDevice" = "যন্ত্র থেকে লোড করুন"; +"newExperimentQR" = "QR কোড থেকে পরীক্ষা যোগ করুন"; +"newExperimentQRscan" = "ফিফক্স পরীক্ষা সম্বলিত QR কোড স্ক্যান করুন।"; +"newExperimentQRErrorTitle" = "QR কোড ত্রুটি।"; +"newExperimentQRNoExperiment" = "ফিফক্স পরীক্ষা সম্বলিত QR কোড পাওয়া যায় নি।"; +"newExperimentQRcrcMismatch" = "আপনি এখন যে QR কোড স্ক্যান করেছেন সেটি আগের কোডের সাথে মিলছে না। দয়া করে একই পরীক্ষা সংক্রান্ত QR কোড স্ক্যান করুন।"; +"newExperimentQRCodesMissing1" = "যে QR কোডটি আপনি এখন স্ক্যান করেছেন সেটি একটি কোডের"; +"newExperimentQRCodesMissing2" = "সংগ্রহের অংশ। বাকি কোডের জন্য \"এগিয়ে যান\" দাবান। অবশিষ্ট কোড:"; +"newExperimentQRBadCRC" = "অবৈধ চেকসাম। এর অর্থ হয় QR কোডটি খারাপ নাহয় স্ক্যান করার সময় কিছু সমস্যা হয়েছে।"; +"newExperimentBTReadErrorTitle" = "ব্লুটুথ পরীক্ষায় ত্রুটি।"; +"newExperimentBTReadErrorCorrupted" = "প্রেরিত পরীক্ষার ডাটা বিকৃত হয়েছে।"; +"generalErrorDialog" = "কোনো সমস্যা হয়েছে।"; +"sensorLinearAcceleration" = "রৈখিক ত্বরণ"; +"sensorLight" = "লাইট"; +"sensorGyroscope" = "জাইরোস্কোপ"; +"sensorAccelerometer" = "ত্বরণমাপি"; +"sensorMagneticField" = "চুম্বক ক্ষেত্র"; +"sensorPressure" = "চাপ"; +"sensorProximity" = "নৈকট্য"; +"sensorTemperature" = "তাপমাত্রা"; +"sensorHumidity" = "আদ্রতা"; +"sensorAttitude" = "দেহভঙ্গিমা"; +"sensorGravity" = "গুরুত্ব"; diff --git a/phyphox-iOS/phyphox/color.xcassets/Color.colorset/Contents.json b/phyphox-iOS/phyphox/color.xcassets/Color.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/phyphox-iOS/phyphox/color.xcassets/Color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/color.xcassets/backgroundDark.colorset/Contents.json b/phyphox-iOS/phyphox/color.xcassets/backgroundDark.colorset/Contents.json new file mode 100644 index 00000000..863f7dbc --- /dev/null +++ b/phyphox-iOS/phyphox/color.xcassets/backgroundDark.colorset/Contents.json @@ -0,0 +1,50 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "gray-gamma-22", + "components" : { + "alpha" : "1.000", + "white" : "0.753" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "gray-gamma-22", + "components" : { + "alpha" : "1.000", + "white" : "0.753" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "gray-gamma-22", + "components" : { + "alpha" : "1.000", + "white" : "0.110" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/color.xcassets/buttonBackground.colorset/Contents.json b/phyphox-iOS/phyphox/color.xcassets/buttonBackground.colorset/Contents.json new file mode 100644 index 00000000..12c62758 --- /dev/null +++ b/phyphox-iOS/phyphox/color.xcassets/buttonBackground.colorset/Contents.json @@ -0,0 +1,34 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "gray-gamma-22", + "components" : { + "alpha" : "1.000", + "white" : "0.665" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "gray-gamma-22", + "components" : { + "alpha" : "1.000", + "white" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phyphox-iOS/phyphox/cs.lproj/Localizable.strings b/phyphox-iOS/phyphox/cs.lproj/Localizable.strings index 4acfdd40..65ed5aef 100644 --- a/phyphox-iOS/phyphox/cs.lproj/Localizable.strings +++ b/phyphox-iOS/phyphox/cs.lproj/Localizable.strings @@ -55,7 +55,7 @@ "sensorGyroscope" = "Gyroskop"; "switchToPhoneLayout" = "Zobrazení: Úzké"; "sensorProximity" = "Senzor přiblížení"; -"unknown" = "neznámé"; +"unknown" = "Neznámé"; "next" = "Další"; "start" = "Start"; "switch_to_calibrated_magnetometer" = "Přepnout na kalibrovaný magnetometr"; @@ -67,7 +67,7 @@ "disableTimedRun" = "Zrušit načasování měření"; "categoryNewExperiment" = "Vlastní jednoduché experimenty"; "confirmDeleteTitle" = "Potvrdit smazání?"; -"pick_exportFormat" = "Zvolte formát dat pro export"; +"pick_exportFormat" = "Zvolte formát dat pro export."; "share_subject" = "Screenshot z phyphoxu"; "title_activity_experiment" = "phyphox"; "save_locally_done" = "Experiment úspěšně přidán do sbírky."; @@ -84,7 +84,7 @@ "remotePhyphoxOrgURL" = "http://phyphox.org/remote-control"; "confirmDelete" = "Chcete tento experiment smazat ze své sbírky?"; "creditsNames" = "Vývoj a koncept:\nDr. Sebastian Staacks\nKoncept:\nProf. Christoph Stampfer\nProgramování:\nGaurav Tripathee\nDominik Dorsel\nJonas Gessner\nCamilla Lummerzheim"; -"remoteServerWarning" = "Právě se chystáte povolit aplikaci vzdálený přístup k naměřeným datům. Prosím ověřte si, že jste připojeni k důvěryhodné síti! Zároveň vás chceme upozornit, že přímé spojení přístrojů není na mnoha univerzitních či firemních sítích povoleno a tedy ani umožněno. \nPro zajištění maximální bezpečnosti a výkonu doporučujeme připojení tohoto zařízení pomocí tetheringu (vytvoření mobilního hotspotu). \nPo zmáčknutí tlačítka Ok můžete svá měření zobrazit jakýmkoliv internetovým prohlížečem na stejné ethernetové síti."; +"remoteServerWarning" = "Právě se chystáte povolit aplikaci vzdálený přístup k naměřeným datům. Prosím ověřte si, že jste připojeni k důvěryhodné síti! Zároveň vás chceme upozornit, že přímé spojení přístrojů není na mnoha univerzitních či firemních sítích povoleno a tedy ani umožněno. \n\nPro zajištění maximální bezpečnosti a výkonu doporučujeme připojení tohoto zařízení pomocí tetheringu (vytvoření mobilního hotspotu). \n\nPo zmáčknutí tlačítka Ok můžete svá měření zobrazit jakýmkoliv internetovým prohlížečem na stejné ethernetové síti."; "export_pick_share" = "Vyberte si způsob exportu"; "sensorNotAvailableWarningTitle" = "Senzor není dostupný."; "sensorNotAvailableWarningMoreInfo" = "Více informací"; @@ -110,7 +110,7 @@ "rename" = "Přejmenovat"; "loadingBluetoothConnectionText" = "Připojuji Bluetoothové zařízení…"; "remoteServerNoNetwork" = "Nejdříve je třeba nastavit Wifi připojení ke vzdálenému zařízení. Prosím povolte Wifi nebo vytvořte mobilní hotspot."; -"fontSize" = "Velikost písma."; +"fontSize" = "Velikost písma"; "start_hint" = "Dotkněte se trojúhelníku pro spuštění experimentu."; "deviceInfo" = "Informace o zařízení"; "copyToClipboard" = "Zkopírovat do schránky"; @@ -144,13 +144,13 @@ "newExperimentBluetoothErrorTitle" = "Chyba Bluetooth skenu"; "newExperimentBluetoothLoadFromDevice" = "Načíst ze zařízení"; "newExperimentBluetoothLoadFromDeviceInfo" = "Toto zařízení obsahuje svůj vlastní phyphox experiment. Zvolte možnost načíst ze zařízení k jeho spuštění."; -"newExperimentQR" = "Přidat experiment pomocí QR kódu."; +"newExperimentQR" = "Přidat experiment pomocí QR kódu"; "newExperimentQRscan" = "Naskenujte QR kód obsahující phyphox experiment."; "newExperimentQRErrorTitle" = "Chyba QR kódu."; "newExperimentQRNoExperiment" = "Nepodařilo se najít QR kód obsahující experiment pro phyphox."; "newExperimentQRcrcMismatch" = "QR kód který jste právě naskenoval/a podle všeho nepatří k předchozím kódům. Prosím ujistěte se, že všechny kódy patří ke stejnému experimentu."; "newExperimentQRCodesMissing1" = "QR kód, který jste právě naskenoval/a, je součástí sady"; -"newExperimentQRCodesMissing2" = "kódů. Prosím stiskněte pokračovat k naskenování zbytku kódů. Stále chybějící kódy:"; +"newExperimentQRCodesMissing2" = "kódy. Prosím stiskněte pokračovat k naskenování zbývajících kódů. Stále chybějící kódy:"; "newExperimentQRBadCRC" = "Neplatný kontrolní součet. To znamená, že buď je QR poškozen nebo nesprávný nebo došlo k chybě při jeho skenování."; "newExperimentBTReadErrorTitle" = "Chyba bluetoothového experimentu."; "newExperimentBTReadErrorCorrupted" = "Přenášená data experimentu jsou poškozena."; @@ -195,7 +195,7 @@ "bt_more_info_link_text" = "Pokud vaše zařízení není podporováno, pak zvolte možnost \"více informací\" abyste se na našich stránkách dozvěděli, jak phyphox podporuje flexibilní Bluetooth Low Energy."; "bt_more_info_link_button" = "Více informací"; "bt_more_info_link_url" = "https://phyphox.org/ble"; -"common_direction_short_north" = "S"; +"common_direction_short_north" = "N"; "common_direction_short_south" = "J"; "common_direction_short_east" = "V"; "common_direction_short_west" = "Z"; @@ -203,33 +203,33 @@ "common_direction_short_south_east" = "JV"; "common_direction_short_north_west" = "SZ"; "common_direction_short_south_west" = "JZ"; -"apple_ban" = "This experiment is no longer available."; -"sensorAttitude" = "Attitude"; -"leave_experiment" = "Leave experiment"; -"leave_experiment_question" = "Leave this experiment and discard recorded data?"; -"leave" = "Leave"; -"export_empty" = "This experiment configuration does not define any data to be exported. If this is an error, please contact the source of the QR code or configuration file."; -"toggleBrightMode" = "Bright mode"; -"networkPrivacyWarning" = "Privacy warning"; -"networkVisitPrivacyURL" = "Privacy policy"; -"networkPrivacyInfo" = "This experiment uses a network connection, which means that experiment data can be transmitted to a network service. You can find more details by clicking the privacy policy button below, which will take you to a website with a privacy policy provided by the creator of this experiment. If the network service is not a local device, you should be aware that phyphox may transmit the following information if you start this experiment:"; -"networkPrivacyUniqueID" = "An id that is unique to this device and the network service, which allow to match all data send from this device through this experiment configuration."; -"networkPrivacySensorData" = "Data from the following sensors:"; -"networkPrivacySensorMicrophone" = "Recordings or data derived from recordings from the microphone."; -"networkPrivacySensorLocation" = "Location data (GPS data or similar)."; -"networkPrivacyDeviceInfo" = "Detailed information about the model of your device and/or the version of phyphox."; -"networkPrivacySensorInfo" = "Technical details about the following sensors:"; -"categoryPhyphoxOrg" = "Contribute to phyphox"; -"categoryPhyphoxOrgHint" = "There are new options to support phyphox at the bottom of the list."; -"graph_tools_system_time" = "Convert to system time"; -"timedRunBeeps" = "Acoustic signals"; -"beeperCountdown" = "Countdown sounds"; -"beeperStart" = "Start sound"; -"beeperRunning" = "Running sounds"; -"beeperStop" = "Stop sound"; -"activate_all" = "Activate all"; -"deactivate_all" = "Deactivate all"; -"common_unit_short_arbitrary_unit" = "a.u."; +"apple_ban" = "Tento experiment již není k dispozici."; +"sensorAttitude" = "Přístup"; +"leave_experiment" = "Opustit experiment"; +"leave_experiment_question" = "Opustit experiment a smazat neuložená data?"; +"leave" = "Opustit"; +"export_empty" = "Tato konfigurace experimentu nemá definována žádná data k exportu. Pokud se jedná o chybu, kontaktujte prosím zdroj QR kódu nebo konfiguračního souboru."; +"toggleBrightMode" = "Světlé prostředí"; +"networkPrivacyWarning" = "Upozornění ohledně soukromí"; +"networkVisitPrivacyURL" = "Zásady ochrany osobních údajů"; +"networkPrivacyInfo" = "Tento experiment používá síťové připojení, což znamená, že data experimentu lze přenést do síťové služby. Další podrobnosti naleznete po kliknutí na tlačítko zásad ochrany osobních údajů níže, které vás přesměruje na webovou stránku se zásadami ochrany osobních údajů poskytnutými tvůrcem tohoto experimentu. Pokud síťová služba není místní zařízení, měli byste si být vědomi toho, že phyphox může přenášet následující informace, pokud spustíte tento experiment:"; +"networkPrivacyUniqueID" = "ID, které je jedinečné pro toto zařízení a síťovou službu, což umožňuje porovnat všechna data odeslaná z tohoto zařízení prostřednictvím této konfigurace experimentu."; +"networkPrivacySensorData" = "Data z následujících senzorů:"; +"networkPrivacySensorMicrophone" = "Nahrávky nebo data odvozená z nahrávek prostřednictvím mikrofonu."; +"networkPrivacySensorLocation" = "Údaje o poloze (GPS nebo podobná data)."; +"networkPrivacyDeviceInfo" = "Detailní informace o modelu vašeho zařízení a/nebo verzi phyphox."; +"networkPrivacySensorInfo" = "Technické informace o následujících senzorech:"; +"categoryPhyphoxOrg" = "Přispějte k phyphox"; +"categoryPhyphoxOrgHint" = "Ve spodní části seznamu jsou nové možnosti podpory phyphox."; +"graph_tools_system_time" = "Převést na systémový čas"; +"timedRunBeeps" = "Akustické signály"; +"beeperCountdown" = "Zvuky odpočítávání"; +"beeperStart" = "Startovací zvuk"; +"beeperRunning" = "Běžící zvuky"; +"beeperStop" = "Zvuk zastavení"; +"activate_all" = "Aktivovat vše"; +"deactivate_all" = "Deaktivovat vše"; +"common_unit_short_arbitrary_unit" = "lib. jednotka"; "common_unit_short_second" = "s"; "common_unit_short_milli_second" = "ms"; "common_unit_short_hertz" = "Hz"; @@ -326,10 +326,18 @@ "common_musical_note_gb7" = "F#7 / Gb7"; "common_musical_note_g7" = "G7"; "common_musical_note_ab7" = "G#7 / Ab7"; -"remoteDepthGUIWarning" = "Previewing and controlling the LiDAR/ToF sensor on the remote interface is not supported."; -"depthAggregationMode" = "Aggregation mode"; -"depthAggregationModePrompt" = "Select the method to aggregate depth information within the selected region."; -"depthAggregationModeClosest" = "Closest"; -"depthAggregationModeAverage" = "Average"; -"sensorDepth" = "Depth (LiDAR)"; -"depthAggregationModeWeighted" = "Weighted"; +"remoteDepthGUIWarning" = "Náhled a ovládání LiDAR/ToF senzoru na dálkovém rozhraní není podporováno."; +"depthAggregationMode" = "Režim agregace"; +"depthAggregationModePrompt" = "Vyberte metodu pro agregaci informací o hloubce ve vybrané oblasti."; +"depthAggregationModeClosest" = "Nejbližší"; +"depthAggregationModeAverage" = "Průměr"; +"sensorDepth" = "Hloubka (LiDAR/ToF)"; +"depthAggregationModeWeighted" = "Vážený průměr"; +"settings" = "Nastavení"; +"sensorGravity" = "Gravitace"; +"sensorCamera" = "Fotoaparát"; +"cameraFrontFacing" = "Přední fotoaparát"; +"cameraBackFacing" = "Zadní fotoaparát"; +"url_invalid" = "Neplatná URL"; +"url_invalid_msg" = "Tento experiment je pouze odkaz, ale nebyla nalezena žádná platná adresa URL."; +"showQRCodeForRemoteURL" = "QR kód k přístupu na URL"; diff --git a/phyphox-iOS/phyphox/el.lproj/Localizable.strings b/phyphox-iOS/phyphox/el.lproj/Localizable.strings index 94f72adc..55b642d2 100644 --- a/phyphox-iOS/phyphox/el.lproj/Localizable.strings +++ b/phyphox-iOS/phyphox/el.lproj/Localizable.strings @@ -337,3 +337,6 @@ "settings" = "Ρυθμίσεις"; "cameraFrontFacing" = "Μπροστινή κάμερα"; "cameraBackFacing" = "Οπίσθια κάμερα"; +"url_invalid" = "Άκυρο URL"; +"url_invalid_msg" = "Αυτό το πείραμα είναι απλά ένας σύνδεσμος, αλλά δεν βρέθηκε έγκυρο URL."; +"showQRCodeForRemoteURL" = "Κωδικός QR για την πρόσβαση σε URL"; diff --git a/phyphox-iOS/phyphox/en.lproj/Localizable.strings b/phyphox-iOS/phyphox/en.lproj/Localizable.strings index 4946a38d..e4aa50e9 100644 --- a/phyphox-iOS/phyphox/en.lproj/Localizable.strings +++ b/phyphox-iOS/phyphox/en.lproj/Localizable.strings @@ -152,6 +152,7 @@ "graph_fit_label" = "Linear fit: y = a x + b"; "remoteColorMapWarning" = "The color plot in the remote interface is only an approximation. In contrast to the in-app plot, at the moment it cannot handle non-equidistant data or logarithmic scaling on the x and y axis. So, data at varying intervals may appear at the wrong location."; "remoteDepthGUIWarning" = "Previewing and controlling the LiDAR/ToF sensor on the remote interface is not supported."; +"remoteCameraWarning" = "Previewing and controlling the camera sensor on the remote interface is not supported."; "applyZoomTitle" = "Apply zoom"; "applyZoomExplanation" = "As you are closing the interactive mode of this graph, you can select if you want to keep your zoom."; "applyZoomAdvanced" = "Advanced options"; @@ -262,6 +263,7 @@ "common_unit_short_hecto_pascal" = "hPa"; "common_unit_short_decibel" = "dB"; "common_unit_short_percent" = "%"; +"common_unit_short_luminance" = "lum"; "common_quantity_short_time" = "t"; "common_quantity_short_angular_velocity" = "ω"; "common_quantity_short_magnetic_field" = "B"; @@ -277,6 +279,7 @@ "common_quantity_short_latitude" = "φ"; "common_quantity_short_longitude" = "λ"; "common_quantity_short_altitude" = "z"; +"common_quantity_short_luminance" = "Lv"; "common_direction_short_north" = "N"; "common_direction_short_south" = "S"; "common_direction_short_east" = "E"; diff --git a/phyphox-iOS/phyphox/ka.lproj/Localizable.strings b/phyphox-iOS/phyphox/ka.lproj/Localizable.strings index 8d0ea431..6814cc34 100644 --- a/phyphox-iOS/phyphox/ka.lproj/Localizable.strings +++ b/phyphox-iOS/phyphox/ka.lproj/Localizable.strings @@ -152,7 +152,7 @@ "pick_exportFormat" = "ამოირჩიე მონაცემების ფორმატი."; "noDrawableData" = "არარის დახატვადი მონაცემები."; "categoryNewExperiment" = "მარტივი ექსპერიმენტები"; -"categoryRawSensor" = "სენსორის მონაცემები"; +"categoryRawSensor" = "დაუმუშავებელი სენსორები"; "categoryPhyphoxOrg" = "წვლილი შეიტანეთ ფაიფოქსში"; "categoryPhyphoxOrgHint" = "ფაიფოქსის დასახმარებლად ახალი ფუნქციებია სიის ბოლოს."; "experimentinfo_hint" = "შეამოწმეთ ექსპერიმენტის ინფორმაცია, იმისათვის რომ გაერკვეთ ექპერიმენტში."; diff --git a/phyphox-iOS/phyphox/ta.lproj/InfoPlist.strings b/phyphox-iOS/phyphox/ta.lproj/InfoPlist.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/phyphox-iOS/phyphox/ta.lproj/InfoPlist.strings @@ -0,0 +1 @@ + diff --git a/phyphox-iOS/phyphox/ta.lproj/Localizable.strings b/phyphox-iOS/phyphox/ta.lproj/Localizable.strings new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/phyphox-iOS/phyphox/ta.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/phyphox-webinterface b/phyphox-webinterface index 650a5934..65a531fe 160000 --- a/phyphox-webinterface +++ b/phyphox-webinterface @@ -1 +1 @@ -Subproject commit 650a593490dbfe47bd98f9fac59b387f8fbc8d92 +Subproject commit 65a531fe56a42b54de65bebe90c8cbb5b22bab4d