From 7fc959e3e1d446b5ba01cfc00adcf5d7588553f8 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sun, 21 Jan 2024 19:13:06 -0800 Subject: [PATCH 01/21] Initial upgrade to latest SpeziBluetooth --- NAMS.xcodeproj/project.pbxproj | 75 ++------- .../xcshareddata/swiftpm/Package.resolved | 87 +++++----- NAMS/Devices/BioPot/Biopot.swift | 75 ++++++--- NAMS/Devices/BioPot/BiopotDevice.swift | 154 +++++++----------- .../BioPot/Model/AccelerometerSample.swift | 1 + NAMS/Devices/BioPot/Model/ByteCodable.swift | 22 --- .../BioPot/Model/DataAcquisition.swift | 1 + NAMS/Devices/BioPot/Model/DataControl.swift | 1 + .../BioPot/Model/DeviceConfiguration.swift | 1 + .../BioPot/Model/DeviceInformation.swift | 1 + NAMS/Devices/BioPot/Model/EEGSample.swift | 1 + .../BioPot/Model/SamplingConfiguration.swift | 1 + NAMS/Devices/DevicesSheet.swift | 15 +- NAMS/Devices/NearbyDevices.swift | 8 + NAMS/EEG/Chart/EEGRecording.swift | 21 ++- NAMS/EEG/Chart/StartRecordingView.swift | 11 +- NAMS/NAMSAppDelegate.swift | 20 +-- ...tientListModel+QuestionnaireResponse.swift | 2 +- NAMS/Patients/Tasks/CompletedTask.swift | 2 +- NAMS/Patients/Tasks/Questionnaire+NAMS.swift | 2 +- NAMS/Resources/Localizable.xcstrings | 18 ++ NAMS/ScheduleView.swift | 25 ++- NAMS/Tiles/ScreeningTile.swift | 2 +- NAMS/Tiles/TilesView.swift | 19 ++- NAMS/Utils/Testing/BiopotDevicePreview.swift | 7 +- 25 files changed, 282 insertions(+), 290 deletions(-) delete mode 100644 NAMS/Devices/BioPot/Model/ByteCodable.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 5829bc2..7baea39 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -75,7 +75,6 @@ 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6329EDD883004B9AB4 /* SpeziAccount */; }; 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6629EDD894004B9AB4 /* SpeziContact */; }; - 2FE5DC6A29EDD8A9004B9AB4 /* SpeziFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC6929EDD8A9004B9AB4 /* SpeziFHIR */; }; 2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7429EDD8E6004B9AB4 /* SpeziFirebaseAccount */; }; 2FE5DC7729EDD8E6004B9AB4 /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7629EDD8E6004B9AB4 /* SpeziFirebaseConfiguration */; }; 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7829EDD8E6004B9AB4 /* SpeziFirestore */; }; @@ -130,7 +129,6 @@ A926D7AE2AB7A552000C4C2F /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A926D76D2AB7A552000C4C2F /* SpeziFirebaseAccount */; }; A926D7B02AB7A552000C4C2F /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7652AB7A552000C4C2F /* Spezi */; }; A926D7B12AB7A552000C4C2F /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = A926D77A2AB7A552000C4C2F /* SpeziViews */; }; - A926D7B32AB7A552000C4C2F /* SpeziFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = A926D76B2AB7A552000C4C2F /* SpeziFHIR */; }; A926D7B42AB7A552000C4C2F /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7712AB7A552000C4C2F /* SpeziOnboarding */; }; A926D7B52AB7A552000C4C2F /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7702AB7A552000C4C2F /* SpeziFirestore */; }; A926D7B62AB7A552000C4C2F /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = A926D76F2AB7A552000C4C2F /* SpeziFirebaseConfiguration */; }; @@ -263,8 +261,6 @@ A9F2ECCE2AEC58B00057C7DD /* SimpleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */; }; A9F2ECD02AEC5EF50057C7DD /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; A9F2ECD12AEC5EF50057C7DD /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; - A9F916752B07FD58007CECF6 /* ByteCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F916742B07FD58007CECF6 /* ByteCodable.swift */; }; - A9F916762B07FD58007CECF6 /* ByteCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F916742B07FD58007CECF6 /* ByteCodable.swift */; }; A9F916782B080967007CECF6 /* BiopotDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F916772B080967007CECF6 /* BiopotDeviceTests.swift */; }; A9FCE8342AE9CA4F0008EA2B /* PatientInformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */; }; /* End PBXBuildFile section */ @@ -411,7 +407,6 @@ A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTile.swift; sourceTree = ""; }; A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTile.swift; sourceTree = ""; }; A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTask.swift; sourceTree = ""; }; - A9F916742B07FD58007CECF6 /* ByteCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteCodable.swift; sourceTree = ""; }; A9F916772B080967007CECF6 /* BiopotDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDeviceTests.swift; sourceTree = ""; }; A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientInformationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -432,7 +427,6 @@ 2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */, 2F49B7762980407C00BCB272 /* Spezi in Frameworks */, 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */, - 2FE5DC6A29EDD8A9004B9AB4 /* SpeziFHIR in Frameworks */, 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */, 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */, 2FE5DC7729EDD8E6004B9AB4 /* SpeziFirebaseConfiguration in Frameworks */, @@ -469,7 +463,6 @@ A988FEBF2B05C7B800022A61 /* SpeziPersonalInfo in Frameworks */, A926D7B02AB7A552000C4C2F /* Spezi in Frameworks */, A926D7B12AB7A552000C4C2F /* SpeziViews in Frameworks */, - A926D7B32AB7A552000C4C2F /* SpeziFHIR in Frameworks */, A988FEA82B03FB5A00022A61 /* SpeziBluetooth in Frameworks */, A926D7B42AB7A552000C4C2F /* SpeziOnboarding in Frameworks */, A926D7B52AB7A552000C4C2F /* SpeziFirestore in Frameworks */, @@ -769,7 +762,6 @@ children = ( A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */, A988FEB42B0453E100022A61 /* DeviceInformation.swift */, - A9F916742B07FD58007CECF6 /* ByteCodable.swift */, A907DA2F2B192FD500FB69FB /* DataControl.swift */, A907DA322B193C9700FB69FB /* SamplingConfiguration.swift */, A907DA352B1942B800FB69FB /* DataAcquisition.swift */, @@ -882,7 +874,6 @@ 2F49B7752980407B00BCB272 /* Spezi */, 2FE5DC6329EDD883004B9AB4 /* SpeziAccount */, 2FE5DC6629EDD894004B9AB4 /* SpeziContact */, - 2FE5DC6929EDD8A9004B9AB4 /* SpeziFHIR */, 2FE5DC7429EDD8E6004B9AB4 /* SpeziFirebaseAccount */, 2FE5DC7629EDD8E6004B9AB4 /* SpeziFirebaseConfiguration */, 2FE5DC7829EDD8E6004B9AB4 /* SpeziFirestore */, @@ -958,7 +949,6 @@ A926D7652AB7A552000C4C2F /* Spezi */, A926D7672AB7A552000C4C2F /* SpeziAccount */, A926D7692AB7A552000C4C2F /* SpeziContact */, - A926D76B2AB7A552000C4C2F /* SpeziFHIR */, A926D76D2AB7A552000C4C2F /* SpeziFirebaseAccount */, A926D76F2AB7A552000C4C2F /* SpeziFirebaseConfiguration */, A926D7702AB7A552000C4C2F /* SpeziFirestore */, @@ -1012,7 +1002,6 @@ 2F49B7742980407B00BCB272 /* XCRemoteSwiftPackageReference "Spezi" */, 2FE5DC6229EDD883004B9AB4 /* XCRemoteSwiftPackageReference "SpeziAccount" */, 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */, - 2FE5DC6829EDD8A9004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFHIR" */, 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */, 2FE5DC7F29EDD91D004B9AB4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */, 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */, @@ -1224,7 +1213,6 @@ A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */, 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, - A9F916752B07FD58007CECF6 /* ByteCodable.swift in Sources */, 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */, 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */, ); @@ -1356,7 +1344,6 @@ A9F2ECCA2AEC2C300057C7DD /* CompletedTile.swift in Sources */, 2DC17686B3AEB09A8F60AB8E /* PatientList.swift in Sources */, 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */, - A9F916762B07FD58007CECF6 /* ByteCodable.swift in Sources */, 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */, 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */, ); @@ -2096,72 +2083,64 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/Spezi"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.8.0; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC6229EDD883004B9AB4 /* XCRemoteSwiftPackageReference "SpeziAccount" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.7.3; + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; }; }; 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.5.1; - }; - }; - 2FE5DC6829EDD8A9004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFHIR" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziFHIR.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.1; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.8.0; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC7F29EDD91D004B9AB4 /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.7.0; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziQuestionnaire.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.5.0; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC8829EDD972004B9AB4 /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.5.0; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.6.2; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -2204,14 +2183,6 @@ minimumVersion = 0.3.3; }; }; - A926D76C2AB7A552000C4C2F /* XCRemoteSwiftPackageReference "SpeziFHIR" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziFHIR.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.4.0; - }; - }; A926D76E2AB7A552000C4C2F /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; @@ -2264,8 +2235,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.2.0; + branch = "feature/client-model"; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -2286,11 +2257,6 @@ package = 2FE5DC6529EDD894004B9AB4 /* XCRemoteSwiftPackageReference "SpeziContact" */; productName = SpeziContact; }; - 2FE5DC6929EDD8A9004B9AB4 /* SpeziFHIR */ = { - isa = XCSwiftPackageProductDependency; - package = 2FE5DC6829EDD8A9004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFHIR" */; - productName = SpeziFHIR; - }; 2FE5DC7429EDD8E6004B9AB4 /* SpeziFirebaseAccount */ = { isa = XCSwiftPackageProductDependency; package = 2FE5DC7329EDD8E6004B9AB4 /* XCRemoteSwiftPackageReference "SpeziFirebase" */; @@ -2351,11 +2317,6 @@ package = A926D76A2AB7A552000C4C2F /* XCRemoteSwiftPackageReference "SpeziContact" */; productName = SpeziContact; }; - A926D76B2AB7A552000C4C2F /* SpeziFHIR */ = { - isa = XCSwiftPackageProductDependency; - package = A926D76C2AB7A552000C4C2F /* XCRemoteSwiftPackageReference "SpeziFHIR" */; - productName = SpeziFHIR; - }; A926D76D2AB7A552000C4C2F /* SpeziFirebaseAccount */ = { isa = XCSwiftPackageProductDependency; package = A926D76E2AB7A552000C4C2F /* XCRemoteSwiftPackageReference "SpeziFirebase" */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3b544b2..9950beb 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "5746b2d35c91c50581590ed97abe4c06b5037274", - "version" : "10.18.0" + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "5de0369ee79ad096c164eb3afeb7921d92a43b58", - "version" : "10.18.0" + "revision" : "b880ec8ec927a838c51c12862c6222c30d7097d7", + "version" : "10.20.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "6b332152355c372ace9966d8ee76ed191f97025e", - "version" : "10.17.0" + "revision" : "ceec9f28dea12b7cf3dabf18b5ed7621c88fd4aa", + "version" : "10.20.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleDataTransport.git", "state" : { - "revision" : "aae45a320fd0d11811820335b1eabc8753902a40", - "version" : "9.2.5" + "revision" : "a732a4b47f59e4f725a2ea10f0c77e93a7131117", + "version" : "9.3.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", - "version" : "3.1.1" + "revision" : "115f75e43851774934d695449a4836123c3246e1", + "version" : "3.2.0" } }, { @@ -132,7 +132,7 @@ "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { "revision" : "7dc09f7acd7fb19673594e0fdd4d72d0869ee006", - "version" : "0.2.3" + "version" : "1.0.0" } }, { @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "092eabc50a3600d8a03b43ad0d2dcd02914b223f", - "version" : "0.8.1" + "revision" : "c4bf0e99de40acfdd2baf0fa02769f06a4c3f0eb", + "version" : "1.1.0" } }, { @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", "state" : { - "revision" : "d65ad46827f748af7f6c99bd783f74225b530747", - "version" : "0.7.3" + "revision" : "714f01ae1e67bf9c1c0e7c07624380f9bea772b7", + "version" : "1.1.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { - "revision" : "ae2d554434431ea5be5fc85a8dba8d55aa54c159", - "version" : "0.2.0" + "branch" : "feature/client-model", + "revision" : "6113cf3991665ef9ff1468c3f70c1291be8bec39" } }, { @@ -167,17 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziContact.git", "state" : { - "revision" : "07515418259c68d6f5058a45b9e70ad0a0706680", - "version" : "0.5.1" - } - }, - { - "identity" : "spezifhir", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziFHIR.git", - "state" : { - "revision" : "043243181f380dfd88ff3cccaab73d42c850c275", - "version" : "0.4.1" + "revision" : "494b776f8c98d771e4a609a1fb706097dba4c030", + "version" : "1.0.0" } }, { @@ -185,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", "state" : { - "revision" : "084d697f58dc1a3275677992f6a5884736e3e2f6", - "version" : "0.8.0" + "revision" : "ca1edf678ec59e76c9869ee3448e6e165d9c2789", + "version" : "1.0.0" } }, { @@ -194,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "683c66f922a4cfe0882c4a86a43854f613b48541", - "version" : "0.1.0" + "revision" : "d1e6d4cddcf236038d21a73d671806d8ba51b01c", + "version" : "1.0.1" } }, { @@ -203,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding.git", "state" : { - "revision" : "70d1a740a0da8a5bc83d95bcff177ebd3e7402c0", - "version" : "0.7.0" + "revision" : "3ee713576eaeaa03200ba26bbc1269ceeb6abb25", + "version" : "1.0.1" } }, { @@ -212,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", "state" : { - "revision" : "4f3e1cd25c271244a4f8fe3653969f17063603a6", - "version" : "0.5.0" + "revision" : "930a4099db1aca9db0b6ed4e77687141c4780052", + "version" : "1.0.0" } }, { @@ -221,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziStorage.git", "state" : { - "revision" : "e9be3c2743e462894bf56d41339b040f4060b567", - "version" : "0.5.0" + "revision" : "eaed2220375c35400aa69d1f96a8d32b7e66b1c7", + "version" : "1.0.0" } }, { @@ -230,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "eac443080926649d09a703483a6dd6f5a8bb7d51", - "version" : "0.6.2" + "revision" : "0137e69d156bf4001a8d6bf5661c9a37b2bbd0aa", + "version" : "1.0.0" } }, { @@ -248,8 +239,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version" : "1.0.5" + "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", + "version" : "1.0.6" } }, { @@ -266,8 +257,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "07f7f26ded8df9645c072f220378879c4642e063", - "version" : "1.25.1" + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" } }, { @@ -275,8 +266,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", "state" : { - "revision" : "388a6d6a5be48eff5d98a2c45e0b50f30ed21dc3", - "version" : "0.4.7" + "revision" : "fb7fcee97c574b950e03b0a53874e26db27db2fe", + "version" : "0.4.8" } }, { @@ -284,8 +275,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", "state" : { - "revision" : "9226052589b8faece98861bc3d7b33b3ebfe4f5a", - "version" : "0.2.5" + "revision" : "bb2a287c2544aa846e53670d1ece35e5949567be", + "version" : "1.0.0" } } ], diff --git a/NAMS/Devices/BioPot/Biopot.swift b/NAMS/Devices/BioPot/Biopot.swift index 46baf2c..4d9aa7a 100644 --- a/NAMS/Devices/BioPot/Biopot.swift +++ b/NAMS/Devices/BioPot/Biopot.swift @@ -13,26 +13,31 @@ import SwiftUI struct Biopot: View { + @Environment(Bluetooth.self) + private var bluetooth @Environment(BiopotDevice.self) - private var biopot + private var biopot: BiopotDevice? @State private var viewState: ViewState = .idle var body: some View { + ListRow("Nearby Devices") { + Text(verbatim: "\(bluetooth.nearbyDevices(for: BiopotDevice.self).count)") + } ListRow("Device") { - Text(biopot.bluetoothState.localizedStringResource) + if let biopot { + Text(biopot.state.localizedStringResource) + } else { + Text("Scanning ...") + } } .viewStateAlert(state: $viewState) - .onChange(of: biopot.bluetoothState) { - if biopot.bluetoothState != .connected { - biopot.deviceInfo = nil - } - } + .scanNearbyDevices(with: bluetooth, autoConnect: true) testingSupport - - if let info = biopot.deviceInfo { + if let biopot, + let info = biopot.service.deviceInfo { Section("Status") { ListRow("BATTERY") { BatteryIcon(percentage: Int(info.batteryLevel)) @@ -50,7 +55,7 @@ struct Biopot: View { } actionButtons - } else if biopot.bluetoothState == .scanning { + } else { Section { ProgressView() .listRowBackground(Color.clear) @@ -63,6 +68,9 @@ struct Biopot: View { @MainActor @ViewBuilder private var testingSupport: some View { if FeatureFlags.testBiopot { Button("Receive Device Info") { + // TODO: allow testing support via a different SPI? + // TODO: also this needs to inject a device instance? + /* biopot.deviceInfo = DeviceInformation( syncRatio: 0, syncMode: false, @@ -71,7 +79,7 @@ struct Biopot: View { batteryLevel: 80, temperatureValue: 23, batteryCharging: false - ) + )*/ } } } @@ -79,16 +87,16 @@ struct Biopot: View { @MainActor @ViewBuilder private var actionButtons: some View { Section("Actions") { // section of testing actions AsyncButton("Read Device Configuration", state: $viewState) { - try biopot.readBiopot(characteristic: BiopotDevice.Characteristic.biopotDeviceConfiguration) + try await biopot?.service.$deviceInfo.read() } AsyncButton("Read Data Control", state: $viewState) { - try biopot.readBiopot(characteristic: BiopotDevice.Characteristic.biopotDataControl) + try await biopot?.service.$dataControl.read() } AsyncButton("Read Data Acquisition", state: $viewState) { - try biopot.readBiopot(characteristic: BiopotDevice.Characteristic.biopotImpedanceMeasurement) + try await biopot?.service.$impedanceMeasurement.read() } AsyncButton("Read Sample Configuration", state: $viewState) { - try biopot.readBiopot(characteristic: BiopotDevice.Characteristic.biopotSamplingConfiguration) + try await biopot?.service.$samplingConfiguration.read() } } } @@ -98,16 +106,31 @@ struct Biopot: View { extension BluetoothState: CustomLocalizedStringResourceConvertible { public var localizedStringResource: LocalizedStringResource { switch self { - case .connected: - return "Connected" - case .disconnected: - return "Disconnected" - case .scanning: - return "Scanning ..." + case .unknown: + "Unknown" + case .poweredOn: + "Bluetooth On" + case .unsupported: + "Bluetooth Unsupported" case .poweredOff: - return "Bluetooth Off" + "Bluetooth Off" case .unauthorized: - return "Bluetooth Unauthorized" + "Bluetooth Unauthorized" + } + } +} + +extension PeripheralState: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .connected: + "Connected" + case .disconnected: + "Disconnected" + case .connecting: + "Connecting" + case .disconnecting: + "Disconnecting" } } } @@ -118,6 +141,10 @@ extension BluetoothState: CustomLocalizedStringResourceConvertible { List { Biopot() } - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } #endif diff --git a/NAMS/Devices/BioPot/BiopotDevice.swift b/NAMS/Devices/BioPot/BiopotDevice.swift index 3610992..abf2df5 100644 --- a/NAMS/Devices/BioPot/BiopotDevice.swift +++ b/NAMS/Devices/BioPot/BiopotDevice.swift @@ -6,37 +6,54 @@ // SPDX-License-Identifier: MIT // -import Combine import NIOCore import OSLog import Spezi import SpeziBluetooth +import class CoreBluetooth.CBUUID import SwiftUI -/// Model for the BioPot 3 device. +class BiopotService: BluetoothService { + @Characteristic(id: .biopotDeviceInfoCharacteristic, notify: true) + var deviceInfo: DeviceInformation? + + @Characteristic(id: .biopotDeviceConfigurationCharacteristic, notify: true) + var deviceConfiguration: DeviceConfiguration? + @Characteristic(id: .biopotSamplingConfigurationCharacteristic) + var samplingConfiguration: SamplingConfiguration? + @Characteristic(id: .biopotDataControlCharacteristic) + var dataControl: DataControl? + + @Characteristic(id: .biopotDataAcquisitionCharacteristic) + var dataAcquisition: Data? // TODO: ByteBuffer doesn't really make sense to by ByteDecodable? + @Characteristic(id: .biopotImpedanceMeasurementCharacteristic) + var impedanceMeasurement: Data? // TODO: find a type for it! + + init() {} +} + + +/// The BioPot 3 bluetooth device. /// /// If you need more information about bluetooth, you might find these resources helpful: /// * https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model /// * https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth /// * https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial -@Observable -class BiopotDevice: Module, EnvironmentAccessible, BluetoothMessageHandler, DefaultInitializable { +class BiopotDevice: BluetoothDevice { private let logger = Logger(subsystem: "edu.stanford.nams", category: "BiopotDevice") - @ObservationIgnored @Dependency private var bluetooth: Bluetooth - - var bluetoothState: BluetoothState { - bluetooth.state - } + @DeviceState(\.state) + var state var connected: Bool { - bluetoothState == .connected + state == .connected } - @MainActor var deviceInfo: DeviceInformation? - @MainActor var deviceConfiguration: DeviceConfiguration? - @MainActor var samplingConfiguration: SamplingConfiguration? + // TODO: add a device information service! + + @Service(id: .biopotService) + var service = BiopotService() @MainActor var startDate: Date? @@ -46,71 +63,21 @@ class BiopotDevice: Module, EnvironmentAccessible, BluetoothMessageHandler, Defa self._recordingSession = .constant(nil) } - - func configure() { - bluetooth.add(messageHandler: self) - } - - func associate(_ model: EEGViewModel) { + func associate(_ model: EEGViewModel) { // TODO: handle this everytime it gets newly created? self._recordingSession = model.recordingSessionBinding } - func recieve(_ data: Data, service: CBUUID, characteristic: CBUUID) async { - guard service == Service.biopot else { - logger.warning("Received data for unknown service: \(service)") - return - } - - logger.warning("Received data for biopot on service \(service.uuidString) for characteristic \(characteristic.uuidString): \(data.hexString())") - - var buffer = ByteBuffer(data: data) - - if characteristic == Characteristic.biopotDeviceInfo { - guard let information = DeviceInformation(from: &buffer) else { - return - } - - await MainActor.run { - self.deviceInfo = information - } - } else if characteristic == Characteristic.biopotDeviceConfiguration { - guard let configuration = DeviceConfiguration(from: &buffer) else { - return - } - - await MainActor.run { - self.deviceConfiguration = configuration - } - } else if characteristic == Characteristic.biopotSamplingConfiguration { - guard let configuration = SamplingConfiguration(from: &buffer) else { - return - } - - await MainActor.run { - self.samplingConfiguration = configuration - } - } else if characteristic == Characteristic.biopotDataAcquisition { - await handleDataAcquisition(buffer: &buffer) - } else { - logger.warning("Data on \(characteristic.uuidString)@\(service.uuidString) was unexpected and not processed!") - } - } - - func readBiopot(characteristic: CBUUID) throws { - try bluetooth.read(service: Service.biopot, characteristic: characteristic) - } - func enableRecording() async { do { try await setDataControl(false) - try bluetooth.read(service: Service.biopot, characteristic: Characteristic.biopotDeviceConfiguration) + _ = try await service.$deviceConfiguration.read() // TODO: use the result? await MainActor.run { startDate = .now } try await setDataControl(true) - try bluetooth.read(service: Service.biopot, characteristic: Characteristic.biopotSamplingConfiguration) + _ = try await service.$samplingConfiguration.read() } catch { logger.error("Failed to enable Biopot recording: \(error)") } @@ -118,20 +85,18 @@ class BiopotDevice: Module, EnvironmentAccessible, BluetoothMessageHandler, Defa func setDataControl(_ enable: Bool) async throws { let control = DataControl(dataAcquisitionEnabled: enable) - var buffer = ByteBuffer() - control.encode(to: &buffer) - - try await bluetooth.write(&buffer, service: Service.biopot, characteristic: Characteristic.biopotDataControl) + _ = try await service.$dataControl.write(control) // TODO: what's the response here? } + // TODO: actually call this handler on change of @Characteristic! private func handleDataAcquisition(buffer: inout ByteBuffer) async { - guard let deviceConfiguration = await deviceConfiguration else { + guard let deviceConfiguration = service.deviceConfiguration else { logger.warning("Received data acquisition without having device configuration ready!") return } guard deviceConfiguration.dataSize == 24 - && deviceConfiguration.channelCount == 8 else { + && deviceConfiguration.channelCount == 8 else { logger.error("Unable to process data acquisition. Unexpected configuration: \(String(describing: deviceConfiguration))") return } @@ -179,36 +144,29 @@ class BiopotDevice: Module, EnvironmentAccessible, BluetoothMessageHandler, Defa } -extension BiopotDevice { - enum Service { - static let biopot = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB") - } -} - - -extension BiopotDevice { - /// Characteristic definitions with access properties. - /// - /// Access properties: R: read, W: write, N: notify - enum Characteristic { // naming is currently guess work - /// Characteristic 1, as per the manual. RWN. - static let biopotDeviceConfiguration = CBUUID(string: "0000FFF1-0000-1000-8000-00805F9B34FB") - /// Characteristic 2, as per the manual. RW. - static let biopotDataControl = CBUUID(string: "0000FFF2-0000-1000-8000-00805F9B34FB") - /// Characteristic 3, as per the manual. RW. - static let biopotImpedanceMeasurement = CBUUID(string: "0000FFF3-0000-1000-8000-00805F9B34FB") - /// Characteristic 4, as per the manual. RN. - static let biopotDataAcquisition = CBUUID(string: "0000FFF4-0000-1000-8000-00805F9B34FB") - /// Characteristic 5, as per the manual. RW. - static let biopotSamplingConfiguration = CBUUID(string: "0000FFF5-0000-1000-8000-00805F9B34FB") - /// Characteristic 6, as per the manual. RN. - static let biopotDeviceInfo = CBUUID(string: "0000FFF6-0000-1000-8000-00805F9B34FB") - } +extension CBUUID { + static let biopotService = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB") + + // Access properties: R: read, W: write, N: notify + // naming is currently guess work + + /// Characteristic 1, as per the manual. RWN. + static let biopotDeviceConfigurationCharacteristic = CBUUID(string: "0000FFF1-0000-1000-8000-00805F9B34FB") + /// Characteristic 2, as per the manual. RW. + static let biopotDataControlCharacteristic = CBUUID(string: "0000FFF2-0000-1000-8000-00805F9B34FB") + /// Characteristic 3, as per the manual. RW. + static let biopotImpedanceMeasurementCharacteristic = CBUUID(string: "0000FFF3-0000-1000-8000-00805F9B34FB") + /// Characteristic 4, as per the manual. RN. + static let biopotDataAcquisitionCharacteristic = CBUUID(string: "0000FFF4-0000-1000-8000-00805F9B34FB") + /// Characteristic 5, as per the manual. RW. + static let biopotSamplingConfigurationCharacteristic = CBUUID(string: "0000FFF5-0000-1000-8000-00805F9B34FB") + /// Characteristic 6, as per the manual. RN. + static let biopotDeviceInfoCharacteristic = CBUUID(string: "0000FFF6-0000-1000-8000-00805F9B34FB") } extension Data { - func hexString() -> String { + func hexString() -> String { // TODO: is this part of XCTBluetooth? or just Bluetooth? map { String(format: "%02hhx", $0) }.joined() } } diff --git a/NAMS/Devices/BioPot/Model/AccelerometerSample.swift b/NAMS/Devices/BioPot/Model/AccelerometerSample.swift index 260ccb3..fca8e79 100644 --- a/NAMS/Devices/BioPot/Model/AccelerometerSample.swift +++ b/NAMS/Devices/BioPot/Model/AccelerometerSample.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth struct Point { diff --git a/NAMS/Devices/BioPot/Model/ByteCodable.swift b/NAMS/Devices/BioPot/Model/ByteCodable.swift deleted file mode 100644 index 3755072..0000000 --- a/NAMS/Devices/BioPot/Model/ByteCodable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import NIOCore - - -protocol ByteDecodable { - init?(from byteBuffer: inout ByteBuffer) -} - - -protocol ByteEncodable { - func encode(to byteBuffer: inout ByteBuffer) -} - - -typealias ByteCodable = ByteEncodable & ByteDecodable diff --git a/NAMS/Devices/BioPot/Model/DataAcquisition.swift b/NAMS/Devices/BioPot/Model/DataAcquisition.swift index 4a12c4c..1e5e73d 100644 --- a/NAMS/Devices/BioPot/Model/DataAcquisition.swift +++ b/NAMS/Devices/BioPot/Model/DataAcquisition.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth struct DataAcquisition10: DataAcquisition { diff --git a/NAMS/Devices/BioPot/Model/DataControl.swift b/NAMS/Devices/BioPot/Model/DataControl.swift index 259a6ef..03882c6 100644 --- a/NAMS/Devices/BioPot/Model/DataControl.swift +++ b/NAMS/Devices/BioPot/Model/DataControl.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth struct DataControl { diff --git a/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift b/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift index 1bd6ce8..4ce05dd 100644 --- a/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift +++ b/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth enum AccelerometerStatus: UInt8, Equatable { diff --git a/NAMS/Devices/BioPot/Model/DeviceInformation.swift b/NAMS/Devices/BioPot/Model/DeviceInformation.swift index 5cb906c..8a785ae 100644 --- a/NAMS/Devices/BioPot/Model/DeviceInformation.swift +++ b/NAMS/Devices/BioPot/Model/DeviceInformation.swift @@ -8,6 +8,7 @@ import Foundation import NIOCore +import SpeziBluetooth struct DeviceInformation { diff --git a/NAMS/Devices/BioPot/Model/EEGSample.swift b/NAMS/Devices/BioPot/Model/EEGSample.swift index 32b587f..0064150 100644 --- a/NAMS/Devices/BioPot/Model/EEGSample.swift +++ b/NAMS/Devices/BioPot/Model/EEGSample.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth struct EEGChannelSample { // we always deal with 24-bits channel samples diff --git a/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift b/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift index 2753f5c..097f2bf 100644 --- a/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift +++ b/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift @@ -7,6 +7,7 @@ // import NIOCore +import SpeziBluetooth struct SamplingConfiguration { diff --git a/NAMS/Devices/DevicesSheet.swift b/NAMS/Devices/DevicesSheet.swift index 6071d6a..7df5012 100644 --- a/NAMS/Devices/DevicesSheet.swift +++ b/NAMS/Devices/DevicesSheet.swift @@ -78,6 +78,19 @@ struct DevicesSheet: View { #if DEBUG #Preview { DevicesSheet(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } +} + +#Preview { + DevicesSheet(eegModel: EEGViewModel(deviceManager: MockDeviceManager(nearbyDevices: []))) + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } #endif diff --git a/NAMS/Devices/NearbyDevices.swift b/NAMS/Devices/NearbyDevices.swift index 62ce75e..7b3cf1e 100644 --- a/NAMS/Devices/NearbyDevices.swift +++ b/NAMS/Devices/NearbyDevices.swift @@ -148,4 +148,12 @@ struct NearbyDevices: View { } } } + +#Preview { + NavigationStack { + List { + NearbyDevices(eegModel: EEGViewModel(deviceManager: MockDeviceManager(nearbyDevices: []))) + } + } +} #endif diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index 7fd3e0f..6123924 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -7,6 +7,7 @@ // import Charts +import SpeziBluetooth import SpeziOnboarding import SpeziViews import SwiftUI @@ -17,7 +18,7 @@ struct EEGRecording: View { private var dismiss private let eegModel: EEGViewModel - @Environment(BiopotDevice.self) + @Environment(BiopotDevice.self) // TODO: make optional private var biopot @Environment(PatientListModel.self) private var patientList @@ -118,7 +119,11 @@ struct EEGRecording: View { return NavigationStack { EEGRecording(eegModel: model) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } } @@ -127,7 +132,11 @@ struct EEGRecording: View { return NavigationStack { EEGRecording(eegModel: EEGViewModel(mock: device)) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } } @@ -135,7 +144,11 @@ struct EEGRecording: View { NavigationStack { EEGRecording(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } } #endif diff --git a/NAMS/EEG/Chart/StartRecordingView.swift b/NAMS/EEG/Chart/StartRecordingView.swift index e8e5ad9..179ab5a 100644 --- a/NAMS/EEG/Chart/StartRecordingView.swift +++ b/NAMS/EEG/Chart/StartRecordingView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziBluetooth import SpeziOnboarding import SwiftUI @@ -13,7 +14,7 @@ import SwiftUI struct StartRecordingView: View { private let eegModel: EEGViewModel @Environment(BiopotDevice.self) - private var biopot + private var biopot: BiopotDevice? var body: some View { OnboardingView( @@ -42,7 +43,7 @@ struct StartRecordingView: View { actionText: "Start Recording", action: { eegModel.startRecordingSession() - if biopot.connected { + if let biopot, biopot.connected { Task { await biopot.enableRecording() } @@ -62,6 +63,10 @@ struct StartRecordingView: View { #if DEBUG #Preview { StartRecordingView(eegModel: EEGViewModel(mock: MockEEGDevice(name: "Device 1", model: "Mock", state: .connected))) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } #endif diff --git a/NAMS/NAMSAppDelegate.swift b/NAMS/NAMSAppDelegate.swift index 6d94426..b085140 100644 --- a/NAMS/NAMSAppDelegate.swift +++ b/NAMS/NAMSAppDelegate.swift @@ -34,22 +34,10 @@ class NAMSAppDelegate: SpeziAppDelegate { } firestore - Bluetooth(services: [ - // we currently only subscribe to biopot-specific characteristics - BluetoothService( - serviceUUID: BiopotDevice.Service.biopot, - characteristicUUIDs: [ - BiopotDevice.Characteristic.biopotDeviceConfiguration, - BiopotDevice.Characteristic.biopotDataControl, - BiopotDevice.Characteristic.biopotImpedanceMeasurement, - BiopotDevice.Characteristic.biopotDataAcquisition, - BiopotDevice.Characteristic.biopotSamplingConfiguration, - BiopotDevice.Characteristic.biopotDeviceInfo - ] - ) - ]) - - BiopotDevice() + Bluetooth { + // TODO: can this be based on the type of BiopotDevice service property? + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } } } diff --git a/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift index 3cc51e9..5a7b332 100644 --- a/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift +++ b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift @@ -6,8 +6,8 @@ // SPDX-License-Identifier: MIT // -import SpeziFHIR import SpeziFirestore +import SpeziQuestionnaire // SpeziFHIR defines the Observation Model which collides with Apples Observation framework naming diff --git a/NAMS/Patients/Tasks/CompletedTask.swift b/NAMS/Patients/Tasks/CompletedTask.swift index 3b0ba64..faf7bd0 100644 --- a/NAMS/Patients/Tasks/CompletedTask.swift +++ b/NAMS/Patients/Tasks/CompletedTask.swift @@ -7,7 +7,7 @@ // import FirebaseFirestoreSwift -import SpeziFHIR +import SpeziQuestionnaire enum TaskContent { diff --git a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift index 73a7b43..9751fbb 100644 --- a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift +++ b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift @@ -7,7 +7,7 @@ // import Foundation -import SpeziFHIR +import SpeziQuestionnaire extension Questionnaire { diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index deec62b..eaf1a83 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -175,9 +175,15 @@ }, "Bluetooth Off" : { + }, + "Bluetooth On" : { + }, "Bluetooth Unauthorized" : { + }, + "Bluetooth Unsupported" : { + }, "BLUETOOTH_OFF" : { "localizations" : { @@ -314,6 +320,9 @@ } } } + }, + "Connecting" : { + }, "CONNECTING" : { "localizations" : { @@ -423,6 +432,9 @@ } } } + }, + "Disconnecting" : { + }, "Done" : { "localizations" : { @@ -754,6 +766,9 @@ } } } + }, + "Nearby Devices" : { + }, "NEARBY_DEVICES" : { "localizations" : { @@ -1218,6 +1233,9 @@ } } } + }, + "Unknown" : { + }, "WEARING" : { "localizations" : { diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index f23b1b1..a19a0c9 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -7,6 +7,7 @@ // import SpeziAccount +import SpeziBluetooth import SwiftUI @@ -17,7 +18,7 @@ struct ScheduleView: View { @State var eegModel = EEGViewModel(deviceManager: MockDeviceManager()) #endif @Environment(BiopotDevice.self) - private var biopot + private var biopot: BiopotDevice? @State var presentingMuseList = false @State var presentPatientSheet = false @@ -55,7 +56,7 @@ struct ScheduleView: View { PatientListSheet(activePatientId: $activePatientId) } .onAppear { - biopot.associate(eegModel) + biopot?.associate(eegModel) // TODO: that has to change! } .toolbar { toolbar @@ -96,15 +97,27 @@ struct ScheduleView: View { #if DEBUG #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant(nil)) - .environment(Account(MockUserIdPasswordAccountService())) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + AccountConfiguration { + MockUserIdPasswordAccountService() + } + } } #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1")) - .environment(Account(MockUserIdPasswordAccountService())) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + AccountConfiguration { + MockUserIdPasswordAccountService() + } + } } #endif diff --git a/NAMS/Tiles/ScreeningTile.swift b/NAMS/Tiles/ScreeningTile.swift index 50fecc7..1ac57bc 100644 --- a/NAMS/Tiles/ScreeningTile.swift +++ b/NAMS/Tiles/ScreeningTile.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SpeziFHIR +import SpeziQuestionnaire import SwiftUI diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index 39a8c24..08c06cb 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -6,17 +6,20 @@ // SPDX-License-Identifier: MIT // +import SpeziBluetooth import SpeziQuestionnaire import SpeziViews import SwiftUI +extension Questionnaire: Identifiable {} // TODO: move somewhere! + @MainActor struct TilesView: View { private let eegModel: EEGViewModel @Environment(BiopotDevice.self) - private var biopot + private var biopot: BiopotDevice? @Environment(PatientListModel.self) private var patientList @@ -44,7 +47,7 @@ struct TilesView: View { MeasurementTile( task: measurement, presentingEEGRecording: $presentingEEGRecording, - deviceConnected: eegModel.activeDevice != nil || biopot.connected + deviceConnected: eegModel.activeDevice != nil || biopot?.connected == true ) } } @@ -104,12 +107,20 @@ struct TilesView: View { patientList.completedTasks = [] return TilesView(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) .environment(patientList) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } #Preview { TilesView(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) .environment(PatientListModel()) - .biopotPreviewSetup() + .previewWith { + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } } #endif diff --git a/NAMS/Utils/Testing/BiopotDevicePreview.swift b/NAMS/Utils/Testing/BiopotDevicePreview.swift index fa225aa..c99278d 100644 --- a/NAMS/Utils/Testing/BiopotDevicePreview.swift +++ b/NAMS/Utils/Testing/BiopotDevicePreview.swift @@ -14,15 +14,16 @@ import SwiftUI private class PreviewDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - Bluetooth() - BiopotDevice() + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } } } } extension View { - func biopotPreviewSetup() -> some View { + func biopotPreviewSetup2() -> some View { self .spezi(PreviewDelegate()) } From a0b51d60a523d33f07251ba46ce6e24c8fb32f4a Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 24 Jan 2024 22:27:02 -0800 Subject: [PATCH 02/21] Initital devce info sheet --- NAMS.xcodeproj/project.pbxproj | 24 ++++- .../xcshareddata/swiftpm/Package.resolved | 21 ++-- NAMS/Account/AccountButton.swift | 2 +- NAMS/Account/AccountSetupHeader.swift | 2 +- NAMS/Account/AccountSheet.swift | 12 ++- NAMS/Bluetooth/BluetoothManager.swift | 4 +- NAMS/Devices/BioPot/Biopot.swift | 68 +++++++++--- NAMS/Devices/BioPot/BiopotDevice.swift | 102 +++++++++++------- .../BioPot/Model/AccelerometerSample.swift | 10 +- .../BioPot/Model/ByteBuffer+Int24.swift | 4 +- .../BioPot/Model/DataAcquisition.swift | 12 +-- NAMS/Devices/BioPot/Model/DataControl.swift | 23 ++-- .../BioPot/Model/DeviceConfiguration.swift | 57 ++++++---- .../BioPot/Model/DeviceInformation.swift | 32 +++--- NAMS/Devices/BioPot/Model/EEGSample.swift | 6 +- .../BioPot/Model/ImpedanceMeasurement.swift | 49 +++++++++ .../BioPot/Model/SamplingConfiguration.swift | 77 +++++++++---- .../BioPot/Recording/EEGChannel+Biopot.swift | 4 +- NAMS/Devices/EEGDeviceDetails.swift | 4 +- NAMS/Devices/EEGDeviceList.swift | 4 +- NAMS/Devices/EEGDeviceRow.swift | 4 +- .../Mock/EEGMeasurementGenerator.swift | 4 +- .../Mock/EEGViewModel+ActiveMock.swift | 4 +- NAMS/Devices/Mock/MockDeviceManager.swift | 4 +- NAMS/Devices/Mock/MockEEGDevice.swift | 4 +- NAMS/Devices/Model/ConnectedDevice.swift | 4 +- NAMS/Devices/Model/ConnectionState.swift | 4 +- .../Model/DeviceConnectionListener.swift | 4 +- NAMS/Devices/Model/DeviceManager.swift | 4 +- NAMS/Devices/Model/EEGDevice.swift | 4 +- NAMS/Devices/Model/Fit.swift | 4 +- NAMS/Devices/Model/HeadbandFit.swift | 4 +- .../IXNMuseConfiguration+Description.swift | 4 +- .../Extensions/IXNMuseModel+Description.swift | 4 +- .../IXNMusePreset+Description.swift | 4 +- .../Extensions/IXNMuseVersion+String.swift | 4 +- NAMS/Devices/Muse/IXNMuse+EEGDevice.swift | 4 +- .../Muse/Model/ConnectionState+Muse.swift | 4 +- .../Devices/Muse/Model/HeadbandFit+Muse.swift | 4 +- .../Model/IXNMuseDataPacketType+Type.swift | 4 +- .../Devices/Muse/MuseConnectionListener.swift | 4 +- NAMS/Devices/Muse/MuseDeviceManager.swift | 4 +- .../Muse/Recording/EEGChannel+Muse.swift | 4 +- .../Muse/Recording/EEGReading+Muse.swift | 5 +- .../Muse/Recording/EEGSeries+Muse.swift | 4 +- NAMS/EEG/Chart/EEGChannelMark.swift | 4 +- NAMS/EEG/Chart/EEGChart.swift | 4 +- NAMS/EEG/Chart/EEGRecording.swift | 10 +- NAMS/EEG/Chart/StartRecordingView.swift | 4 +- NAMS/EEG/EEGRecordingSession.swift | 4 +- NAMS/EEG/Recording/EEGChannel.swift | 4 +- NAMS/EEG/Recording/EEGFrequency.swift | 4 +- NAMS/EEG/Recording/EEGReading.swift | 4 +- NAMS/EEG/Recording/EEGSeries.swift | 4 +- NAMS/Home.swift | 6 +- NAMS/Onboarding/AccountOnboarding.swift | 12 ++- NAMS/Onboarding/NotificationPermissions.swift | 2 +- .../OnboardingFlow+PreviewSimulator.swift | 3 +- NAMS/Patients/Model/NewPatientModel.swift | 4 +- NAMS/Patients/Model/PatientListModel.swift | 4 +- NAMS/Patients/PatientList.swift | 4 +- NAMS/Patients/SelectedPatientCard.swift | 4 +- NAMS/Patients/Tasks/CompletedTask.swift | 4 +- NAMS/Patients/Tasks/PatientTask.swift | 4 +- NAMS/Resources/Localizable.xcstrings | 27 +++-- NAMS/Supporting Files/NAMS-Bridging-Header.h | 4 +- NAMS/Tiles/TileType.swift | 4 +- .../Helper/ProcessInfo+PreviewSimulator.swift | 3 +- NAMS/Utils/ListRow.swift | 16 ++- NAMS/Utils/Testing/BiopotDevicePreview.swift | 4 +- NAMSTests/BiopotCodingTests.swift | 3 + NAMSTests/BiopotDeviceTests.swift | 21 ++-- NAMSUITests/BiopotTests.swift | 4 +- NAMSUITests/QuestionnaireTests.swift | 2 +- 74 files changed, 511 insertions(+), 284 deletions(-) create mode 100644 NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 7baea39..d23d0a1 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -242,6 +242,10 @@ A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */; }; A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */; }; + A9C82F922B608756004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; + A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; + A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F942B6089C8004703E0 /* BluetoothServices */; }; + A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F962B6089D2004703E0 /* BluetoothServices */; }; A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */; }; A9CE84512B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */; }; A9CE84522B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */; }; @@ -399,6 +403,7 @@ A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientListModel.swift; sourceTree = ""; }; A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentPatientLabel.swift; sourceTree = ""; }; A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoInformationText.swift; sourceTree = ""; }; + A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImpedanceMeasurement.swift; sourceTree = ""; }; A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGDeviceTests.swift; sourceTree = ""; }; A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesSheet.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; @@ -426,6 +431,7 @@ A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */, 2FE5DC7529EDD8E6004B9AB4 /* SpeziFirebaseAccount in Frameworks */, 2F49B7762980407C00BCB272 /* Spezi in Frameworks */, + A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */, 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */, 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */, 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */, @@ -463,6 +469,7 @@ A988FEBF2B05C7B800022A61 /* SpeziPersonalInfo in Frameworks */, A926D7B02AB7A552000C4C2F /* Spezi in Frameworks */, A926D7B12AB7A552000C4C2F /* SpeziViews in Frameworks */, + A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */, A988FEA82B03FB5A00022A61 /* SpeziBluetooth in Frameworks */, A926D7B42AB7A552000C4C2F /* SpeziOnboarding in Frameworks */, A926D7B52AB7A552000C4C2F /* SpeziFirestore in Frameworks */, @@ -768,6 +775,7 @@ A907DA382B195D4800FB69FB /* AccelerometerSample.swift */, A907DA3B2B195ED800FB69FB /* EEGSample.swift */, A907DA3E2B1964B500FB69FB /* ByteBuffer+Int24.swift */, + A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */, ); path = Model; sourceTree = ""; @@ -885,6 +893,7 @@ A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */, A988FEBC2B05C7AE00022A61 /* SpeziPersonalInfo */, A988FEA52B03FB4A00022A61 /* SpeziBluetooth */, + A9C82F942B6089C8004703E0 /* BluetoothServices */, ); productName = NAMS; productReference = 653A254D283387FE005D4D48 /* NAMS.app */; @@ -960,6 +969,7 @@ A92E34F12ADB9B9000FE0B51 /* OrderedCollections */, A988FEBE2B05C7B800022A61 /* SpeziPersonalInfo */, A988FEA72B03FB5A00022A61 /* SpeziBluetooth */, + A9C82F962B6089D2004703E0 /* BluetoothServices */, ); productName = NAMS; productReference = A926D7C32AB7A552000C4C2F /* NAMS Muse.app */; @@ -1115,6 +1125,7 @@ files = ( A907DA3C2B195ED800FB69FB /* EEGSample.swift in Sources */, A94534002AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, + A9C82F922B608756004703E0 /* ImpedanceMeasurement.swift in Sources */, A926D8302AB7B430000C4C2F /* EEGViewModel.swift in Sources */, A94A42AE2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, A926D7FF2AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, @@ -1288,6 +1299,7 @@ A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */, A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */, A988FEB62B0453E100022A61 /* DeviceInformation.swift in Sources */, + A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */, A9F2ECD12AEC5EF50057C7DD /* MeasurementTask.swift in Sources */, A9BCB5832AE8307800DA8588 /* SearchToken.swift in Sources */, A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */, @@ -2235,7 +2247,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; requirement = { - branch = "feature/client-model"; + branch = "feature/unit-testing-setup"; kind = branch; }; }; @@ -2387,6 +2399,16 @@ package = 2FE5DC8D29EDD980004B9AB4 /* XCRemoteSwiftPackageReference "SpeziViews" */; productName = SpeziPersonalInfo; }; + A9C82F942B6089C8004703E0 /* BluetoothServices */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothServices; + }; + A9C82F962B6089D2004703E0 /* BluetoothServices */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothServices; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 653A2545283387FE005D4D48 /* Project object */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9950beb..2d836d7 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { - "branch" : "feature/client-model", - "revision" : "6113cf3991665ef9ff1468c3f70c1291be8bec39" + "branch" : "feature/unit-testing-setup", + "revision" : "f9db62d3b53475b52cc6f56ed92242482dd4f72d" } }, { @@ -194,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding.git", "state" : { - "revision" : "3ee713576eaeaa03200ba26bbc1269ceeb6abb25", - "version" : "1.0.1" + "revision" : "8fb6d9f1a080661c0cc564a93b82ead3c8d44d4f", + "version" : "1.0.2" } }, { @@ -248,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "702cd7c56d5d44eeba73fdf83918339b26dc855c", - "version" : "2.62.0" + "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", + "version" : "2.63.0" } }, { @@ -261,6 +261,15 @@ "version" : "1.25.2" } }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, { "identity" : "xctestextensions", "kind" : "remoteSourceControl", diff --git a/NAMS/Account/AccountButton.swift b/NAMS/Account/AccountButton.swift index 67351ef..eadf1ab 100644 --- a/NAMS/Account/AccountButton.swift +++ b/NAMS/Account/AccountButton.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // diff --git a/NAMS/Account/AccountSetupHeader.swift b/NAMS/Account/AccountSetupHeader.swift index 2d77849..b59ec83 100644 --- a/NAMS/Account/AccountSetupHeader.swift +++ b/NAMS/Account/AccountSetupHeader.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // diff --git a/NAMS/Account/AccountSheet.swift b/NAMS/Account/AccountSheet.swift index 774fb1a..75c7b37 100644 --- a/NAMS/Account/AccountSheet.swift +++ b/NAMS/Account/AccountSheet.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // @@ -72,11 +72,17 @@ struct AccountSheet: View { .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) return AccountSheet() - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #Preview { AccountSheet() - .environment(Account(MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration { + MockUserIdPasswordAccountService() + } + } } #endif diff --git a/NAMS/Bluetooth/BluetoothManager.swift b/NAMS/Bluetooth/BluetoothManager.swift index ddcb761..1a37c94 100644 --- a/NAMS/Bluetooth/BluetoothManager.swift +++ b/NAMS/Bluetooth/BluetoothManager.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/BioPot/Biopot.swift b/NAMS/Devices/BioPot/Biopot.swift index 4d9aa7a..67d5e82 100644 --- a/NAMS/Devices/BioPot/Biopot.swift +++ b/NAMS/Devices/BioPot/Biopot.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -21,18 +21,44 @@ struct Biopot: View { @State private var viewState: ViewState = .idle var body: some View { - ListRow("Nearby Devices") { - Text(verbatim: "\(bluetooth.nearbyDevices(for: BiopotDevice.self).count)") + let devices = bluetooth.nearbyDevices(for: BiopotDevice.self) + + // TODO: We need some place to put our modifiers! + Section { + Text("Make sure your device is connected and nearby!") + .listRowBackground(Color.clear) + .listRowInsets(.init(top: 0, leading: 0.2, bottom: 0, trailing: 0.2)) + .viewStateAlert(state: $viewState) + .scanNearbyDevices(with: bluetooth, autoConnect: true) } - ListRow("Device") { - if let biopot { - Text(biopot.state.localizedStringResource) - } else { - Text("Scanning ...") + + if devices.isEmpty { + VStack { // TODO: Reuse! + Text("Searching for nearby devices ...") + .foregroundColor(.secondary) + ProgressView() + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } else { + Section { + ForEach(devices) { device in + if let name = device.name { + ListRow(name) { + Text(device.state.localizedStringResource) + } + } + } + } header: { + HStack { // TODO: reuse! + Text("Devices") + .padding(.trailing, 10) + if bluetooth.isScanning { + ProgressView() + } + } } } - .viewStateAlert(state: $viewState) - .scanNearbyDevices(with: bluetooth, autoConnect: true) testingSupport @@ -52,10 +78,25 @@ struct Biopot: View { ListRow("Temperature") { Text("\(info.temperatureValue) °C") } + if let serialNumber = biopot.deviceInformation.serialNumber { + ListRow("Serial Number") { + Text(serialNumber) + } + } + if let firmwareVersion = biopot.deviceInformation.firmwareRevision { + ListRow("Firmware Version") { + Text(firmwareVersion) + } + } + if let hardwareVersion = biopot.deviceInformation.hardwareRevision { + ListRow("Hardware Version") { + Text(hardwareVersion) + } + } } actionButtons - } else { + } else if biopot != nil { Section { ProgressView() .listRowBackground(Color.clear) @@ -86,6 +127,9 @@ struct Biopot: View { @MainActor @ViewBuilder private var actionButtons: some View { Section("Actions") { // section of testing actions + AsyncButton("Query Device Information", state: $viewState) { + try await biopot?.deviceInformation.retrieveDeviceInformation() + } AsyncButton("Read Device Configuration", state: $viewState) { try await biopot?.service.$deviceInfo.read() } diff --git a/NAMS/Devices/BioPot/BiopotDevice.swift b/NAMS/Devices/BioPot/BiopotDevice.swift index abf2df5..1223211 100644 --- a/NAMS/Devices/BioPot/BiopotDevice.swift +++ b/NAMS/Devices/BioPot/BiopotDevice.swift @@ -1,11 +1,12 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // +import BluetoothServices import NIOCore import OSLog import Spezi @@ -24,11 +25,11 @@ class BiopotService: BluetoothService { var samplingConfiguration: SamplingConfiguration? @Characteristic(id: .biopotDataControlCharacteristic) var dataControl: DataControl? + @Characteristic(id: .biopotImpedanceMeasurementCharacteristic) + var impedanceMeasurement: ImpedanceMeasurement? @Characteristic(id: .biopotDataAcquisitionCharacteristic) - var dataAcquisition: Data? // TODO: ByteBuffer doesn't really make sense to by ByteDecodable? - @Characteristic(id: .biopotImpedanceMeasurementCharacteristic) - var impedanceMeasurement: Data? // TODO: find a type for it! + var dataAcquisition: Data? // either `DataAcquisition10` or `DataAcquisition11` depending on the configuration. init() {} } @@ -40,56 +41,74 @@ class BiopotService: BluetoothService { /// * https://en.wikipedia.org/wiki/Bluetooth_Low_Energy#Software_model /// * https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth /// * https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial -class BiopotDevice: BluetoothDevice { +class BiopotDevice: BluetoothDevice, Identifiable { private let logger = Logger(subsystem: "edu.stanford.nams", category: "BiopotDevice") + @DeviceState(\.id) + var id @DeviceState(\.state) var state + @DeviceState(\.name) + var name var connected: Bool { state == .connected } - // TODO: add a device information service! + @Service(id: .deviceInformationService) + var deviceInformation = DeviceInformationService() // TODO: make sure we read once we are connected! @Service(id: .biopotService) var service = BiopotService() @MainActor var startDate: Date? - @Binding @ObservationIgnored private var recordingSession: EEGRecordingSession? + @Binding private var recordingSession: EEGRecordingSession? required init() { self._recordingSession = .constant(nil) + + service.$dataAcquisition + .onChange(perform: handleDataAcquisition) } func associate(_ model: EEGViewModel) { // TODO: handle this everytime it gets newly created? self._recordingSession = model.recordingSessionBinding } + private func handleChange(of state: PeripheralState) { + if case .connected = state { + Task { @MainActor in + try? await Task.sleep(for: .milliseconds(500)) // TODO: better timing? + logger.debug("Querying device information!") + do { + try await deviceInformation.retrieveDeviceInformation() + } catch { + logger.error("Failed to retrieve device information: \(error)") + } + } + } + } + func enableRecording() async { do { - try await setDataControl(false) + try await service.$dataControl.write(false) - _ = try await service.$deviceConfiguration.read() // TODO: use the result? + // make sure the value is up to date + _ = try await service.$deviceConfiguration.read() await MainActor.run { startDate = .now } - try await setDataControl(true) + + try await service.$dataControl.write(true) _ = try await service.$samplingConfiguration.read() } catch { logger.error("Failed to enable Biopot recording: \(error)") } } - func setDataControl(_ enable: Bool) async throws { - let control = DataControl(dataAcquisitionEnabled: enable) - _ = try await service.$dataControl.write(control) // TODO: what's the response here? - } - - // TODO: actually call this handler on change of @Characteristic! - private func handleDataAcquisition(buffer: inout ByteBuffer) async { + private func handleDataAcquisition(data: Data) { guard let deviceConfiguration = service.deviceConfiguration else { logger.warning("Received data acquisition without having device configuration ready!") return @@ -101,25 +120,25 @@ class BiopotDevice: BluetoothDevice { return } - let data: DataAcquisition? + let acquisition: DataAcquisition? if case .off = deviceConfiguration.accelerometerStatus { - data = DataAcquisition10(from: &buffer) + acquisition = DataAcquisition10(data: data) } else { - data = DataAcquisition11(from: &buffer) + acquisition = DataAcquisition11(data: data) } - guard let data else { + guard let acquisition else { return } - await MainActor.run { + Task { @MainActor in guard let recordingSession else { return } let baseDate = startDate ?? .now - let series: [EEGSeries] = data.samples.map { sample in + let series: [EEGSeries] = acquisition.samples.map { sample in var readings: [EEGReading] = [] readings.reserveCapacity(8) @@ -134,7 +153,7 @@ class BiopotDevice: BluetoothDevice { // We currently register all samples within a packet at the same timestamp. We might need to research // how each sample within a packet is evenly distributed. - let timestamp = baseDate.addingTimeInterval(Double(data.timestamps) / 1000.0) + let timestamp = baseDate.addingTimeInterval(Double(acquisition.timestamps) / 1000.0) return EEGSeries(timestamp: timestamp, readings: readings) } @@ -144,29 +163,34 @@ class BiopotDevice: BluetoothDevice { } +extension BiopotDevice: Hashable { + static func == (lhs: BiopotDevice, rhs: BiopotDevice) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + + extension CBUUID { - static let biopotService = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB") + static let biopotService = CBUUID(string: "FFF0") // Access properties: R: read, W: write, N: notify // naming is currently guess work /// Characteristic 1, as per the manual. RWN. - static let biopotDeviceConfigurationCharacteristic = CBUUID(string: "0000FFF1-0000-1000-8000-00805F9B34FB") + static let biopotDeviceConfigurationCharacteristic = CBUUID(string: "FFF1") /// Characteristic 2, as per the manual. RW. - static let biopotDataControlCharacteristic = CBUUID(string: "0000FFF2-0000-1000-8000-00805F9B34FB") + static let biopotDataControlCharacteristic = CBUUID(string: "FFF2") /// Characteristic 3, as per the manual. RW. - static let biopotImpedanceMeasurementCharacteristic = CBUUID(string: "0000FFF3-0000-1000-8000-00805F9B34FB") + static let biopotImpedanceMeasurementCharacteristic = CBUUID(string: "FFF3") /// Characteristic 4, as per the manual. RN. - static let biopotDataAcquisitionCharacteristic = CBUUID(string: "0000FFF4-0000-1000-8000-00805F9B34FB") + static let biopotDataAcquisitionCharacteristic = CBUUID(string: "FFF4") /// Characteristic 5, as per the manual. RW. - static let biopotSamplingConfigurationCharacteristic = CBUUID(string: "0000FFF5-0000-1000-8000-00805F9B34FB") + static let biopotSamplingConfigurationCharacteristic = CBUUID(string: "FFF5") + // swiftlint:disable:previous identifier_name /// Characteristic 6, as per the manual. RN. - static let biopotDeviceInfoCharacteristic = CBUUID(string: "0000FFF6-0000-1000-8000-00805F9B34FB") -} - - -extension Data { - func hexString() -> String { // TODO: is this part of XCTBluetooth? or just Bluetooth? - map { String(format: "%02hhx", $0) }.joined() - } + static let biopotDeviceInfoCharacteristic = CBUUID(string: "FFF6") } diff --git a/NAMS/Devices/BioPot/Model/AccelerometerSample.swift b/NAMS/Devices/BioPot/Model/AccelerometerSample.swift index fca8e79..5c6876a 100644 --- a/NAMS/Devices/BioPot/Model/AccelerometerSample.swift +++ b/NAMS/Devices/BioPot/Model/AccelerometerSample.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -29,9 +29,9 @@ extension Point: ByteDecodable { return nil } - guard let x = byteBuffer.readInteger(endianness: .little, as: Int16.self), // swiftlint:disable:this identifier_name - let y = byteBuffer.readInteger(endianness: .little, as: Int16.self), // swiftlint:disable:this identifier_name - let z = byteBuffer.readInteger(endianness: .little, as: Int16.self) else { // swiftlint:disable:this identifier_name + guard let x = Int16(from: &byteBuffer), // swiftlint:disable:this identifier_name + let y = Int16(from: &byteBuffer), // swiftlint:disable:this identifier_name + let z = Int16(from: &byteBuffer) else { // swiftlint:disable:this identifier_name return nil } diff --git a/NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift b/NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift index f7f04a4..efb0eba 100644 --- a/NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift +++ b/NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/BioPot/Model/DataAcquisition.swift b/NAMS/Devices/BioPot/Model/DataAcquisition.swift index 1e5e73d..56ba4eb 100644 --- a/NAMS/Devices/BioPot/Model/DataAcquisition.swift +++ b/NAMS/Devices/BioPot/Model/DataAcquisition.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -32,10 +32,6 @@ protocol DataAcquisition: ByteDecodable { extension DataAcquisition { - fileprivate static func readTimestamp(from byteBuffer: inout ByteBuffer) -> UInt32? { - byteBuffer.readInteger(endianness: .little, as: UInt32.self) - } - // swiftlint:disable:next discouraged_optional_collection fileprivate static func readSamples(from byteBuffer: inout ByteBuffer, count: Int) -> [EEGSample]? { var samples: [EEGSample] = [] @@ -59,7 +55,7 @@ extension DataAcquisition10 { return nil } - guard let timestamps = Self.readTimestamp(from: &byteBuffer), + guard let timestamps = UInt32(from: &byteBuffer), let samples = Self.readSamples(from: &byteBuffer, count: 10) else { return nil } @@ -76,7 +72,7 @@ extension DataAcquisition11 { return nil } - guard let timestamps = Self.readTimestamp(from: &byteBuffer), + guard let timestamps = UInt32(from: &byteBuffer), let samples = Self.readSamples(from: &byteBuffer, count: 9), let accelerometerSample = AccelerometerSample(from: &byteBuffer) else { return nil diff --git a/NAMS/Devices/BioPot/Model/DataControl.swift b/NAMS/Devices/BioPot/Model/DataControl.swift index 03882c6..eb8e653 100644 --- a/NAMS/Devices/BioPot/Model/DataControl.swift +++ b/NAMS/Devices/BioPot/Model/DataControl.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -19,22 +19,23 @@ struct DataControl { } +extension DataControl: ExpressibleByBooleanLiteral { + init(booleanLiteral value: BooleanLiteralType) { + self.init(dataAcquisitionEnabled: value) + } +} + + extension DataControl: ByteCodable { init?(from byteBuffer: inout ByteBuffer) { - guard byteBuffer.readableBytes >= 1 else { + guard let value = Bool(from: &byteBuffer) else { return nil } - guard let dataAcquisitionEnabled = byteBuffer.readInteger(as: UInt8.self) else { - return nil - } - - self.dataAcquisitionEnabled = dataAcquisitionEnabled == 1 + self.dataAcquisitionEnabled = value } func encode(to byteBuffer: inout ByteBuffer) { - byteBuffer.reserveCapacity(1) - - byteBuffer.writeInteger(dataAcquisitionEnabled ? 1 : 0, as: UInt8.self) + dataAcquisitionEnabled.encode(to: &byteBuffer) } } diff --git a/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift b/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift index 4ce05dd..b617f70 100644 --- a/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift +++ b/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -32,6 +32,20 @@ struct DeviceConfiguration { } +extension AccelerometerStatus: ByteCodable { + init?(from byteBuffer: inout ByteBuffer) { + guard let value = UInt8(from: &byteBuffer) else { + return nil + } + self.init(rawValue: value) + } + + func encode(to byteBuffer: inout ByteBuffer) { + rawValue.encode(to: &byteBuffer) + } +} + + extension DeviceConfiguration: ByteCodable, Equatable { init?(from byteBuffer: inout ByteBuffer) { guard byteBuffer.readableBytes >= 16 else { @@ -40,26 +54,25 @@ extension DeviceConfiguration: ByteCodable, Equatable { byteBuffer.moveReaderIndex(to: 5) // reserved bytes - guard let channelCount = byteBuffer.readInteger(as: UInt8.self), - let accelerometerStatusNum = byteBuffer.readInteger(as: UInt8.self), - let accelerometerStatus = AccelerometerStatus(rawValue: accelerometerStatusNum), - let impedanceStatus = byteBuffer.readInteger(as: UInt8.self), - let memoryStatus = byteBuffer.readInteger(as: UInt8.self), - let samplesPerChannel = byteBuffer.readInteger(as: UInt8.self), - let dataSize = byteBuffer.readInteger(as: UInt8.self), - let syncEnabled = byteBuffer.readInteger(as: UInt8.self), - let serialNumber = byteBuffer.readInteger(as: UInt32.self) else { + guard let channelCount = UInt8(from: &byteBuffer), + let accelerometerStatus = AccelerometerStatus(from: &byteBuffer), + let impedanceStatus = Bool(from: &byteBuffer), + let memoryStatus = Bool(from: &byteBuffer), + let samplesPerChannel = UInt8(from: &byteBuffer), + let dataSize = UInt8(from: &byteBuffer), + let syncEnabled = Bool(from: &byteBuffer), + let serialNumber = byteBuffer.readInteger(endianness: .big, as: UInt32.self) else { return nil } self.channelCount = channelCount self.accelerometerStatus = accelerometerStatus - self.impedanceStatus = impedanceStatus == 1 - self.memoryStatus = memoryStatus == 1 + self.impedanceStatus = impedanceStatus + self.memoryStatus = memoryStatus self.samplesPerChannel = samplesPerChannel self.dataSize = dataSize - self.syncEnabled = syncEnabled == 1 + self.syncEnabled = syncEnabled self.serialNumber = serialNumber } @@ -69,13 +82,13 @@ extension DeviceConfiguration: ByteCodable, Equatable { byteBuffer.writeRepeatingByte(0, count: 5) // reserved bytes, we just write zeros for now - byteBuffer.writeInteger(channelCount) - byteBuffer.writeInteger(accelerometerStatus.rawValue) - byteBuffer.writeInteger(impedanceStatus ? 1 : 0, as: UInt8.self) - byteBuffer.writeInteger(memoryStatus ? 1 : 0, as: UInt8.self) - byteBuffer.writeInteger(samplesPerChannel) - byteBuffer.writeInteger(dataSize) - byteBuffer.writeInteger(syncEnabled ? 1 : 0, as: UInt8.self) - byteBuffer.writeInteger(serialNumber) + channelCount.encode(to: &byteBuffer) + accelerometerStatus.encode(to: &byteBuffer) + impedanceStatus.encode(to: &byteBuffer) + memoryStatus.encode(to: &byteBuffer) + samplesPerChannel.encode(to: &byteBuffer) + dataSize.encode(to: &byteBuffer) + syncEnabled.encode(to: &byteBuffer) + byteBuffer.writeInteger(serialNumber, endianness: .big) } } diff --git a/NAMS/Devices/BioPot/Model/DeviceInformation.swift b/NAMS/Devices/BioPot/Model/DeviceInformation.swift index 8a785ae..dd6f86b 100644 --- a/NAMS/Devices/BioPot/Model/DeviceInformation.swift +++ b/NAMS/Devices/BioPot/Model/DeviceInformation.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -24,28 +24,22 @@ struct DeviceInformation { extension DeviceInformation: ByteDecodable, Equatable { init?(from byteBuffer: inout ByteBuffer) { - guard byteBuffer.readableBytes >= 15 else { + guard let syncRatio = byteBuffer.readInteger(endianness: .big, as: UInt64.self), + let syncMode = Bool(from: &byteBuffer), + let memoryWriteNumber = byteBuffer.readInteger(endianness: .big, as: UInt16.self), + let memoryEraseMode = Bool(from: &byteBuffer), + let batteryLevel = UInt8(from: &byteBuffer), + let temperatureValue = UInt8(from: &byteBuffer), + let batteryCharging = Bool(from: &byteBuffer) else { return nil } - guard let syncRatioData = byteBuffer.readData(length: 8), - let syncMode = byteBuffer.readInteger(as: UInt8.self), - let memoryWriteNumber = byteBuffer.readInteger(as: UInt16.self), - let memoryEraseMode = byteBuffer.readInteger(as: UInt8.self), - let batteryLevel = byteBuffer.readInteger(as: UInt8.self), - let temperatureValue = byteBuffer.readInteger(as: UInt8.self), - let batteryCharging = byteBuffer.readInteger(as: UInt8.self) else { - return nil - } - - self .syncRatio = syncRatioData.withUnsafeBytes { pointer in - pointer.load(as: Double.self) - } - self.syncMode = syncMode == 1 + self.syncRatio = Double(bitPattern: syncRatio) + self.syncMode = syncMode self.memoryWriteNumber = memoryWriteNumber - self.memoryEraseMode = memoryEraseMode == 1 + self.memoryEraseMode = memoryEraseMode self.batteryLevel = batteryLevel self.temperatureValue = temperatureValue - self.batteryCharging = !(batteryCharging == 1) // documentation is wrong, this bit is flipped for some reason + self.batteryCharging = !(batteryCharging) // documentation is wrong, this bit is flipped for some reason } } diff --git a/NAMS/Devices/BioPot/Model/EEGSample.swift b/NAMS/Devices/BioPot/Model/EEGSample.swift index 0064150..9c598fd 100644 --- a/NAMS/Devices/BioPot/Model/EEGSample.swift +++ b/NAMS/Devices/BioPot/Model/EEGSample.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -26,7 +26,7 @@ extension EEGChannelSample: RawRepresentable { } - init?(rawValue: Int32) { + init(rawValue: Int32) { self.init(sample: rawValue) } } diff --git a/NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift b/NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift new file mode 100644 index 0000000..412deea --- /dev/null +++ b/NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift @@ -0,0 +1,49 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import NIO +import SpeziBluetooth + + +struct ImpedanceMeasurement { + let enabled: Bool + let bioImpedanceEnabled: Bool + /// Impedance time in seconds between measurements + let interval: UInt8 + /// 20 impedance values for each channel in 100Ω resolution. + let values: [UInt8] + + + init(enabled: Bool, bioImpedanceEnabled: Bool, interval: UInt8, values: [UInt8]) { + self.enabled = enabled + self.bioImpedanceEnabled = bioImpedanceEnabled + self.interval = interval + self.values = values + } +} + + +extension ImpedanceMeasurement: ByteCodable { + init?(from byteBuffer: inout ByteBuffer) { + guard let enabled = Bool(from: &byteBuffer), + let bioEnabled = Bool(from: &byteBuffer), + let interval = UInt8(from: &byteBuffer), + let values = byteBuffer.readBytes(length: byteBuffer.readableBytes) else { + return nil + } + + self.init(enabled: enabled, bioImpedanceEnabled: bioEnabled, interval: interval, values: values) + } + + func encode(to byteBuffer: inout ByteBuffer) { + enabled.encode(to: &byteBuffer) + bioImpedanceEnabled.encode(to: &byteBuffer) + interval.encode(to: &byteBuffer) + byteBuffer.writeBytes(values) + } +} diff --git a/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift b/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift index 097f2bf..cfa0a07 100644 --- a/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift +++ b/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -124,22 +124,57 @@ extension SamplingConfiguration { // swiftlint:enable identifier_name -extension SamplingConfiguration: ByteCodable { +extension SamplingConfiguration.LowPassFilter: ByteCodable { init?(from byteBuffer: inout ByteBuffer) { - guard byteBuffer.readableBytes >= 10 else { + guard let value = UInt8(from: &byteBuffer) else { return nil } + self.init(rawValue: value) + } + + func encode(to byteBuffer: inout ByteBuffer) { + rawValue.encode(to: &byteBuffer) + } +} - guard let channelsBitMask = byteBuffer.readInteger(as: UInt32.self), - let lowPassFilterValue = byteBuffer.readInteger(as: UInt8.self), - let lowPassFilter = LowPassFilter(rawValue: lowPassFilterValue), - let highPassFilterValue = byteBuffer.readInteger(as: UInt8.self), - let highPassFilter = HighPassFilter(rawValue: highPassFilterValue), - let hardwareSamplingRate = byteBuffer.readInteger(as: UInt16.self), - let impedanceFrequency = byteBuffer.readInteger(as: UInt8.self), - let impedanceScale = byteBuffer.readInteger(as: UInt8.self), - let softwareLowPassFilterValue = byteBuffer.readInteger(as: UInt8.self), - let softwareLowPassFilter = SoftwareLowPassFilter(rawValue: softwareLowPassFilterValue) else { + +extension SamplingConfiguration.HighPassFilter: ByteCodable { + init?(from byteBuffer: inout ByteBuffer) { + guard let value = UInt8(from: &byteBuffer) else { + return nil + } + self.init(rawValue: value) + } + + func encode(to byteBuffer: inout ByteBuffer) { + rawValue.encode(to: &byteBuffer) + } +} + + +extension SamplingConfiguration.SoftwareLowPassFilter: ByteCodable { + init?(from byteBuffer: inout ByteBuffer) { + guard let value = UInt8(from: &byteBuffer) else { + return nil + } + self.init(rawValue: value) + } + + func encode(to byteBuffer: inout ByteBuffer) { + rawValue.encode(to: &byteBuffer) + } +} + + +extension SamplingConfiguration: ByteCodable { + init?(from byteBuffer: inout ByteBuffer) { + guard let channelsBitMask = byteBuffer.readInteger(endianness: .big, as: UInt32.self), + let lowPassFilter = LowPassFilter(from: &byteBuffer), + let highPassFilter = HighPassFilter(from: &byteBuffer), + let hardwareSamplingRate = byteBuffer.readInteger(endianness: .big, as: UInt16.self), + let impedanceFrequency = UInt8(from: &byteBuffer), + let impedanceScale = UInt8(from: &byteBuffer), + let softwareLowPassFilter = SoftwareLowPassFilter(from: &byteBuffer) else { return nil } @@ -155,12 +190,12 @@ extension SamplingConfiguration: ByteCodable { func encode(to byteBuffer: inout ByteBuffer) { byteBuffer.reserveCapacity(10) - byteBuffer.writeInteger(channelsBitMask) - byteBuffer.writeInteger(lowPassFilter.rawValue) - byteBuffer.writeInteger(highPassFilter.rawValue) - byteBuffer.writeInteger(hardwareSamplingRate) - byteBuffer.writeInteger(impedanceFrequency) - byteBuffer.writeInteger(impedanceScale) - byteBuffer.writeInteger(softwareLowPassFilter.rawValue) + byteBuffer.writeInteger(channelsBitMask, endianness: .big) + lowPassFilter.encode(to: &byteBuffer) + highPassFilter.encode(to: &byteBuffer) + byteBuffer.writeInteger(hardwareSamplingRate, endianness: .big) + impedanceFrequency.encode(to: &byteBuffer) + impedanceScale.encode(to: &byteBuffer) + softwareLowPassFilter.encode(to: &byteBuffer) } } diff --git a/NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift b/NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift index 4748c47..9821b74 100644 --- a/NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift +++ b/NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/EEGDeviceDetails.swift b/NAMS/Devices/EEGDeviceDetails.swift index afc9a7f..15a1540 100644 --- a/NAMS/Devices/EEGDeviceDetails.swift +++ b/NAMS/Devices/EEGDeviceDetails.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/EEGDeviceList.swift b/NAMS/Devices/EEGDeviceList.swift index 2858a15..82a5473 100644 --- a/NAMS/Devices/EEGDeviceList.swift +++ b/NAMS/Devices/EEGDeviceList.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/EEGDeviceRow.swift b/NAMS/Devices/EEGDeviceRow.swift index 448802c..4f35c2e 100644 --- a/NAMS/Devices/EEGDeviceRow.swift +++ b/NAMS/Devices/EEGDeviceRow.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Mock/EEGMeasurementGenerator.swift b/NAMS/Devices/Mock/EEGMeasurementGenerator.swift index 8a6eb08..0557fc9 100644 --- a/NAMS/Devices/Mock/EEGMeasurementGenerator.swift +++ b/NAMS/Devices/Mock/EEGMeasurementGenerator.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift b/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift index 0a6fb1f..3ea9913 100644 --- a/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift +++ b/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Mock/MockDeviceManager.swift b/NAMS/Devices/Mock/MockDeviceManager.swift index 662140b..06de8bd 100644 --- a/NAMS/Devices/Mock/MockDeviceManager.swift +++ b/NAMS/Devices/Mock/MockDeviceManager.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Mock/MockEEGDevice.swift b/NAMS/Devices/Mock/MockEEGDevice.swift index 34faa79..40be3ef 100644 --- a/NAMS/Devices/Mock/MockEEGDevice.swift +++ b/NAMS/Devices/Mock/MockEEGDevice.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/ConnectedDevice.swift b/NAMS/Devices/Model/ConnectedDevice.swift index 92ae3b4..f619a80 100644 --- a/NAMS/Devices/Model/ConnectedDevice.swift +++ b/NAMS/Devices/Model/ConnectedDevice.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/ConnectionState.swift b/NAMS/Devices/Model/ConnectionState.swift index b18bfc4..32556b7 100644 --- a/NAMS/Devices/Model/ConnectionState.swift +++ b/NAMS/Devices/Model/ConnectionState.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/DeviceConnectionListener.swift b/NAMS/Devices/Model/DeviceConnectionListener.swift index 8e38f62..46a2cff 100644 --- a/NAMS/Devices/Model/DeviceConnectionListener.swift +++ b/NAMS/Devices/Model/DeviceConnectionListener.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/DeviceManager.swift b/NAMS/Devices/Model/DeviceManager.swift index 5bc0032..1939a83 100644 --- a/NAMS/Devices/Model/DeviceManager.swift +++ b/NAMS/Devices/Model/DeviceManager.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/EEGDevice.swift b/NAMS/Devices/Model/EEGDevice.swift index cef9030..d519225 100644 --- a/NAMS/Devices/Model/EEGDevice.swift +++ b/NAMS/Devices/Model/EEGDevice.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/Fit.swift b/NAMS/Devices/Model/Fit.swift index eedc804..dea2cdc 100644 --- a/NAMS/Devices/Model/Fit.swift +++ b/NAMS/Devices/Model/Fit.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Model/HeadbandFit.swift b/NAMS/Devices/Model/HeadbandFit.swift index d4b08fb..3b2f227 100644 --- a/NAMS/Devices/Model/HeadbandFit.swift +++ b/NAMS/Devices/Model/HeadbandFit.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift b/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift index d33b0c7..eb7e8df 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Extensions/IXNMuseModel+Description.swift b/NAMS/Devices/Muse/Extensions/IXNMuseModel+Description.swift index 79dc207..144d9a5 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMuseModel+Description.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMuseModel+Description.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Extensions/IXNMusePreset+Description.swift b/NAMS/Devices/Muse/Extensions/IXNMusePreset+Description.swift index 01d2396..c89c66b 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMusePreset+Description.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMusePreset+Description.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift b/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift index 28e2ebe..b8fdfcf 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift b/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift index 4004548..f55d5cb 100644 --- a/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift +++ b/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Model/ConnectionState+Muse.swift b/NAMS/Devices/Muse/Model/ConnectionState+Muse.swift index cfcfef3..f7ada13 100644 --- a/NAMS/Devices/Muse/Model/ConnectionState+Muse.swift +++ b/NAMS/Devices/Muse/Model/ConnectionState+Muse.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Model/HeadbandFit+Muse.swift b/NAMS/Devices/Muse/Model/HeadbandFit+Muse.swift index 32d124d..0bacf9f 100644 --- a/NAMS/Devices/Muse/Model/HeadbandFit+Muse.swift +++ b/NAMS/Devices/Muse/Model/HeadbandFit+Muse.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Model/IXNMuseDataPacketType+Type.swift b/NAMS/Devices/Muse/Model/IXNMuseDataPacketType+Type.swift index e022b14..3a37ce7 100644 --- a/NAMS/Devices/Muse/Model/IXNMuseDataPacketType+Type.swift +++ b/NAMS/Devices/Muse/Model/IXNMuseDataPacketType+Type.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/MuseConnectionListener.swift b/NAMS/Devices/Muse/MuseConnectionListener.swift index c5a0861..0baac30 100644 --- a/NAMS/Devices/Muse/MuseConnectionListener.swift +++ b/NAMS/Devices/Muse/MuseConnectionListener.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 6e2dc19..281b7e4 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Recording/EEGChannel+Muse.swift b/NAMS/Devices/Muse/Recording/EEGChannel+Muse.swift index 366989a..019c5fa 100644 --- a/NAMS/Devices/Muse/Recording/EEGChannel+Muse.swift +++ b/NAMS/Devices/Muse/Recording/EEGChannel+Muse.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Devices/Muse/Recording/EEGReading+Muse.swift b/NAMS/Devices/Muse/Recording/EEGReading+Muse.swift index 6768d06..1390dd4 100644 --- a/NAMS/Devices/Muse/Recording/EEGReading+Muse.swift +++ b/NAMS/Devices/Muse/Recording/EEGReading+Muse.swift @@ -1,11 +1,12 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // + #if MUSE extension EEGReading { init(from packet: IXNMuseDataPacket, _ channel: EEGChannel) { diff --git a/NAMS/Devices/Muse/Recording/EEGSeries+Muse.swift b/NAMS/Devices/Muse/Recording/EEGSeries+Muse.swift index 19cb361..883d39b 100644 --- a/NAMS/Devices/Muse/Recording/EEGSeries+Muse.swift +++ b/NAMS/Devices/Muse/Recording/EEGSeries+Muse.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Chart/EEGChannelMark.swift b/NAMS/EEG/Chart/EEGChannelMark.swift index d8fefd1..a38f8ff 100644 --- a/NAMS/EEG/Chart/EEGChannelMark.swift +++ b/NAMS/EEG/Chart/EEGChannelMark.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Chart/EEGChart.swift b/NAMS/EEG/Chart/EEGChart.swift index 2b93d39..8fdfa16 100644 --- a/NAMS/EEG/Chart/EEGChart.swift +++ b/NAMS/EEG/Chart/EEGChart.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index 6123924..103ecf1 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -18,8 +18,8 @@ struct EEGRecording: View { private var dismiss private let eegModel: EEGViewModel - @Environment(BiopotDevice.self) // TODO: make optional - private var biopot + @Environment(BiopotDevice.self) + private var biopot: BiopotDevice? @Environment(PatientListModel.self) private var patientList @@ -32,7 +32,7 @@ struct EEGRecording: View { var body: some View { ZStack { - if !biopot.connected && eegModel.activeDevice == nil { + if !(biopot?.connected ?? false) && eegModel.activeDevice == nil { NoInformationText { Text("No Device connected!") } caption: { diff --git a/NAMS/EEG/Chart/StartRecordingView.swift b/NAMS/EEG/Chart/StartRecordingView.swift index 179ab5a..89cc871 100644 --- a/NAMS/EEG/Chart/StartRecordingView.swift +++ b/NAMS/EEG/Chart/StartRecordingView.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/EEGRecordingSession.swift b/NAMS/EEG/EEGRecordingSession.swift index 5bbd6f6..381c94a 100644 --- a/NAMS/EEG/EEGRecordingSession.swift +++ b/NAMS/EEG/EEGRecordingSession.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Recording/EEGChannel.swift b/NAMS/EEG/Recording/EEGChannel.swift index b9d41f1..cc4f486 100644 --- a/NAMS/EEG/Recording/EEGChannel.swift +++ b/NAMS/EEG/Recording/EEGChannel.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Recording/EEGFrequency.swift b/NAMS/EEG/Recording/EEGFrequency.swift index f157dbf..0f2b7f4 100644 --- a/NAMS/EEG/Recording/EEGFrequency.swift +++ b/NAMS/EEG/Recording/EEGFrequency.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Recording/EEGReading.swift b/NAMS/EEG/Recording/EEGReading.swift index a0159f9..dd27e83 100644 --- a/NAMS/EEG/Recording/EEGReading.swift +++ b/NAMS/EEG/Recording/EEGReading.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/EEG/Recording/EEGSeries.swift b/NAMS/EEG/Recording/EEGSeries.swift index a9d749d..b95d99b 100644 --- a/NAMS/EEG/Recording/EEGSeries.swift +++ b/NAMS/EEG/Recording/EEGSeries.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Home.swift b/NAMS/Home.swift index ef88ab0..4efc9eb 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -26,6 +26,8 @@ struct HomeView: View { @Environment(Account.self) private var account + @Environment(BiopotDevice.self) + private var biopot: BiopotDevice? @State private var patientList = PatientListModel() @@ -103,6 +105,8 @@ struct HomeView: View { .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) return HomeView() - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #endif diff --git a/NAMS/Onboarding/AccountOnboarding.swift b/NAMS/Onboarding/AccountOnboarding.swift index 6a27443..772af87 100644 --- a/NAMS/Onboarding/AccountOnboarding.swift +++ b/NAMS/Onboarding/AccountOnboarding.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // @@ -48,7 +48,11 @@ struct AccountOnboarding: View { #Preview { stack - .environment(Account(MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration { + MockUserIdPasswordAccountService() + } + } } #Preview { let details = AccountDetails.Builder() @@ -56,6 +60,8 @@ struct AccountOnboarding: View { .set(\.name, value: PersonNameComponents(givenName: "Leland", familyName: "Stanford")) return stack - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #endif diff --git a/NAMS/Onboarding/NotificationPermissions.swift b/NAMS/Onboarding/NotificationPermissions.swift index ead11b5..aded3ec 100644 --- a/NAMS/Onboarding/NotificationPermissions.swift +++ b/NAMS/Onboarding/NotificationPermissions.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // diff --git a/NAMS/Onboarding/OnboardingFlow+PreviewSimulator.swift b/NAMS/Onboarding/OnboardingFlow+PreviewSimulator.swift index 160f016..cede143 100644 --- a/NAMS/Onboarding/OnboardingFlow+PreviewSimulator.swift +++ b/NAMS/Onboarding/OnboardingFlow+PreviewSimulator.swift @@ -1,10 +1,11 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // + import SwiftUI diff --git a/NAMS/Patients/Model/NewPatientModel.swift b/NAMS/Patients/Model/NewPatientModel.swift index 557c21d..7c13d5e 100644 --- a/NAMS/Patients/Model/NewPatientModel.swift +++ b/NAMS/Patients/Model/NewPatientModel.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Patients/Model/PatientListModel.swift b/NAMS/Patients/Model/PatientListModel.swift index 31e4c94..a0a5037 100644 --- a/NAMS/Patients/Model/PatientListModel.swift +++ b/NAMS/Patients/Model/PatientListModel.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Patients/PatientList.swift b/NAMS/Patients/PatientList.swift index 2ac6271..61606ab 100644 --- a/NAMS/Patients/PatientList.swift +++ b/NAMS/Patients/PatientList.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Patients/SelectedPatientCard.swift b/NAMS/Patients/SelectedPatientCard.swift index cded542..2527885 100644 --- a/NAMS/Patients/SelectedPatientCard.swift +++ b/NAMS/Patients/SelectedPatientCard.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Patients/Tasks/CompletedTask.swift b/NAMS/Patients/Tasks/CompletedTask.swift index faf7bd0..f1570cd 100644 --- a/NAMS/Patients/Tasks/CompletedTask.swift +++ b/NAMS/Patients/Tasks/CompletedTask.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Patients/Tasks/PatientTask.swift b/NAMS/Patients/Tasks/PatientTask.swift index 598ca19..0d9dddf 100644 --- a/NAMS/Patients/Tasks/PatientTask.swift +++ b/NAMS/Patients/Tasks/PatientTask.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index eaf1a83..13ac029 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -399,6 +399,9 @@ } } } + }, + "Devices" : { + }, "Discard Changes" : { "localizations" : { @@ -527,6 +530,9 @@ } } } + }, + "Firmware Version" : { + }, "FIRMWARE_VERSION" : { "localizations" : { @@ -578,6 +584,9 @@ } } } + }, + "Hardware Version" : { + }, "HEADBAND" : { "localizations" : { @@ -702,6 +711,9 @@ } } } + }, + "Make sure your device is connected and nearby!" : { + }, "Mark completed" : { "localizations" : { @@ -766,9 +778,6 @@ } } } - }, - "Nearby Devices" : { - }, "NEARBY_DEVICES" : { "localizations" : { @@ -971,6 +980,9 @@ } } } + }, + "Query Device Information" : { + }, "Questionnaire" : { "comment" : "Tile Type", @@ -1019,9 +1031,6 @@ } } } - }, - "Scanning ..." : { - }, "Schedule" : { "comment" : "Schedule Title", @@ -1053,6 +1062,9 @@ } } } + }, + "Searching for nearby devices ..." : { + }, "Seconds" : { "localizations" : { @@ -1115,6 +1127,9 @@ } } } + }, + "Serial Number" : { + }, "SERIAL_NUMBER" : { "localizations" : { diff --git a/NAMS/Supporting Files/NAMS-Bridging-Header.h b/NAMS/Supporting Files/NAMS-Bridging-Header.h index 5342469..8b160b2 100644 --- a/NAMS/Supporting Files/NAMS-Bridging-Header.h +++ b/NAMS/Supporting Files/NAMS-Bridging-Header.h @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Tiles/TileType.swift b/NAMS/Tiles/TileType.swift index 400c08b..5b8c9dd 100644 --- a/NAMS/Tiles/TileType.swift +++ b/NAMS/Tiles/TileType.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift b/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift index 1386661..65b1cee 100644 --- a/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift +++ b/NAMS/Utils/Helper/ProcessInfo+PreviewSimulator.swift @@ -1,10 +1,11 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // + import Foundation diff --git a/NAMS/Utils/ListRow.swift b/NAMS/Utils/ListRow.swift index 93fe126..98a1cd5 100644 --- a/NAMS/Utils/ListRow.swift +++ b/NAMS/Utils/ListRow.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // @@ -10,12 +10,12 @@ import SwiftUI struct ListRow: View { - private let name: LocalizedStringResource + private let name: Text private let value: Value var body: some View { HStack { - Text(name) + name Spacer() value .foregroundColor(.secondary) @@ -23,8 +23,14 @@ struct ListRow: View { .accessibilityElement(children: .combine) } + @_disfavoredOverload + init(_ string: String, @ViewBuilder value: () -> Value) { + self.name = Text(verbatim: string) + self.value = value() + } + init(_ name: LocalizedStringResource, @ViewBuilder value: () -> Value) { - self.name = name + self.name = Text(name) self.value = value() } } diff --git a/NAMS/Utils/Testing/BiopotDevicePreview.swift b/NAMS/Utils/Testing/BiopotDevicePreview.swift index c99278d..54704cb 100644 --- a/NAMS/Utils/Testing/BiopotDevicePreview.swift +++ b/NAMS/Utils/Testing/BiopotDevicePreview.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMSTests/BiopotCodingTests.swift b/NAMSTests/BiopotCodingTests.swift index 36a3ef7..559dcba 100644 --- a/NAMSTests/BiopotCodingTests.swift +++ b/NAMSTests/BiopotCodingTests.swift @@ -8,6 +8,9 @@ @testable import NAMS import NIOCore +import SpeziBluetooth +// TODO: @_spi(TestingSupport) import SpeziBluetooth // swiftlint:disable:this attributes +// TODO: import XCTBluetooth import XCTest diff --git a/NAMSTests/BiopotDeviceTests.swift b/NAMSTests/BiopotDeviceTests.swift index b53b380..2443a4e 100644 --- a/NAMSTests/BiopotDeviceTests.swift +++ b/NAMSTests/BiopotDeviceTests.swift @@ -12,25 +12,22 @@ import SpeziBluetooth import SwiftUI import XCTest - +/* private class TestDelegate: SpeziAppDelegate { - let device: BiopotDevice - override var configuration: Configuration { Configuration { - Bluetooth(services: []) - device + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } } } - - - init(device: BiopotDevice) { - self.device = device - } } - + */ final class BiopotDeviceTests: XCTestCase { + // TODO: How to we test our device test? + + /* private var device: BiopotDevice! // swiftlint:disable:this implicitly_unwrapped_optional override func setUpWithError() throws { @@ -60,5 +57,5 @@ final class BiopotDeviceTests: XCTestCase { ) XCTAssertEqual(device.deviceInfo, expected) - } + }*/ } diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index a85aaf2..e88c5a4 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -1,7 +1,7 @@ // -// This source file is part of the Stanford Spezi open-source project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// SPDX-FileCopyrightText: 2023 Stanford University // // SPDX-License-Identifier: MIT // diff --git a/NAMSUITests/QuestionnaireTests.swift b/NAMSUITests/QuestionnaireTests.swift index 52f1234..a2d9362 100644 --- a/NAMSUITests/QuestionnaireTests.swift +++ b/NAMSUITests/QuestionnaireTests.swift @@ -1,5 +1,5 @@ // -// This source file is part of the Stanford Spezi Template Application project +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project // // SPDX-FileCopyrightText: 2023 Stanford University // From 9bbf19104357e44ea654d3c113c41946f8fea41f Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sat, 27 Jan 2024 22:22:07 -0800 Subject: [PATCH 03/21] Restructure device support --- NAMS.xcodeproj/project.pbxproj | 264 ++++++------ .../xcshareddata/swiftpm/Package.resolved | 18 +- NAMS/Bluetooth/BluetoothManager.swift | 40 -- NAMS/Devices/BatteryIcon.swift | 45 +- NAMS/Devices/BioPot/BiopotDevice.swift | 52 ++- .../AccelerometerSample.swift | 0 .../ByteBuffer+Int24.swift | 0 .../DataAcquisition.swift | 0 .../DataControl.swift | 0 .../DeviceConfiguration.swift | 0 .../DeviceInformation.swift | 0 .../EEGSample.swift | 0 .../ImpedanceMeasurement.swift | 0 .../SamplingConfiguration.swift | 0 .../Views/BiopotDeviceDetailsView.swift | 68 +++ .../BioPot/Views/BiopotDeviceRow.swift | 48 +++ NAMS/Devices/DeviceCoordinator.swift | 154 +++++++ NAMS/Devices/DevicesSheet.swift | 96 ----- NAMS/Devices/EEGDeviceList.swift | 41 -- NAMS/Devices/EEGDeviceRow.swift | 162 -------- .../MaybeExternal/BluetoothStateHints.swift | 104 +++++ .../MaybeExternal/LoadingSectionHeader.swift | 54 +++ .../MaybeExternal/NearbyDeviceRow.swift | 184 +++++++++ .../Mock/EEGViewModel+ActiveMock.swift | 18 - NAMS/Devices/Mock/MockDevice.swift | 165 ++++++++ NAMS/Devices/Mock/MockDeviceManager.swift | 62 ++- NAMS/Devices/Mock/MockEEGDevice.swift | 117 ------ ...r.swift => MockMeasurementGenerator.swift} | 2 +- NAMS/Devices/Mock/Views/MockDeviceRow.swift | 51 +++ NAMS/Devices/Model/ConnectedDevice.swift | 84 ---- .../Model/DeviceConnectionListener.swift | 10 - NAMS/Devices/Model/DeviceManager.swift | 20 - NAMS/Devices/Model/EEGDevice.swift | 23 -- NAMS/Devices/Model/EEGViewModel.swift | 120 ------ NAMS/Devices/Muse/IXNMuse+EEGDevice.swift | 43 -- .../{ => Muse}/Model/ConnectionState.swift | 3 + NAMS/Devices/{ => Muse}/Model/Fit.swift | 0 .../{ => Muse}/Model/HeadbandFit.swift | 0 .../Devices/Muse/MuseConnectionListener.swift | 146 ------- NAMS/Devices/Muse/MuseDevice.swift | 341 +++++++++++++++ NAMS/Devices/Muse/MuseDeviceManager.swift | 102 +++-- .../Views/MuseDeviceDetailsView.swift} | 99 +++-- NAMS/Devices/Muse/Views/MuseDeviceList.swift | 41 ++ NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 94 +++++ .../Views/MuseTroublesConnectingHint.swift | 24 ++ NAMS/Devices/NearbyDevices.swift | 159 ------- NAMS/Devices/NearbyDevicesView.swift | 155 +++++++ NAMS/EEG/Chart/EEGChannelMark.swift | 2 +- NAMS/EEG/Chart/EEGChart.swift | 2 +- NAMS/EEG/Chart/EEGRecording.swift | 55 +-- NAMS/EEG/Chart/StartRecordingView.swift | 20 +- NAMS/EEG/EEGRecordingSession.swift | 13 +- NAMS/EEG/EEGRecordings.swift | 48 +++ NAMS/Home.swift | 15 +- NAMS/NAMSAppDelegate.swift | 3 + NAMS/Resources/Localizable.xcstrings | 34 +- .../Resources/SocialSupportQuestionnaire.json | 387 ------------------ NAMS/ScheduleView.swift | 15 +- NAMS/Tiles/MeasurementTile.swift | 2 + NAMS/Tiles/TilesView.swift | 40 +- NAMS/Utils/ListRow.swift | 143 ++++++- NAMS/Utils/NoInformationText.swift | 2 +- NAMS/Utils/StorageKeys.swift | 4 + 63 files changed, 2185 insertions(+), 1809 deletions(-) delete mode 100644 NAMS/Bluetooth/BluetoothManager.swift rename NAMS/Devices/BioPot/{Model => Characteristics}/AccelerometerSample.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/ByteBuffer+Int24.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/DataAcquisition.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/DataControl.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/DeviceConfiguration.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/DeviceInformation.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/EEGSample.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/ImpedanceMeasurement.swift (100%) rename NAMS/Devices/BioPot/{Model => Characteristics}/SamplingConfiguration.swift (100%) create mode 100644 NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift create mode 100644 NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift create mode 100644 NAMS/Devices/DeviceCoordinator.swift delete mode 100644 NAMS/Devices/DevicesSheet.swift delete mode 100644 NAMS/Devices/EEGDeviceList.swift delete mode 100644 NAMS/Devices/EEGDeviceRow.swift create mode 100644 NAMS/Devices/MaybeExternal/BluetoothStateHints.swift create mode 100644 NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift create mode 100644 NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift delete mode 100644 NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift create mode 100644 NAMS/Devices/Mock/MockDevice.swift delete mode 100644 NAMS/Devices/Mock/MockEEGDevice.swift rename NAMS/Devices/Mock/{EEGMeasurementGenerator.swift => MockMeasurementGenerator.swift} (98%) create mode 100644 NAMS/Devices/Mock/Views/MockDeviceRow.swift delete mode 100644 NAMS/Devices/Model/ConnectedDevice.swift delete mode 100644 NAMS/Devices/Model/DeviceConnectionListener.swift delete mode 100644 NAMS/Devices/Model/DeviceManager.swift delete mode 100644 NAMS/Devices/Model/EEGDevice.swift delete mode 100644 NAMS/Devices/Model/EEGViewModel.swift delete mode 100644 NAMS/Devices/Muse/IXNMuse+EEGDevice.swift rename NAMS/Devices/{ => Muse}/Model/ConnectionState.swift (94%) rename NAMS/Devices/{ => Muse}/Model/Fit.swift (100%) rename NAMS/Devices/{ => Muse}/Model/HeadbandFit.swift (100%) delete mode 100644 NAMS/Devices/Muse/MuseConnectionListener.swift create mode 100644 NAMS/Devices/Muse/MuseDevice.swift rename NAMS/Devices/{EEGDeviceDetails.swift => Muse/Views/MuseDeviceDetailsView.swift} (52%) create mode 100644 NAMS/Devices/Muse/Views/MuseDeviceList.swift create mode 100644 NAMS/Devices/Muse/Views/MuseDeviceRow.swift create mode 100644 NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift delete mode 100644 NAMS/Devices/NearbyDevices.swift create mode 100644 NAMS/Devices/NearbyDevicesView.swift create mode 100644 NAMS/EEG/EEGRecordings.swift delete mode 100644 NAMS/Resources/SocialSupportQuestionnaire.json diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index d23d0a1..11fb20f 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -7,61 +7,69 @@ objects = { /* Begin PBXBuildFile section */ - 2DC17128D81F6CF06F65FFD4 /* MuseConnectionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C07ED61540F306E7D23 /* MuseConnectionListener.swift */; }; + 2DC1707B04BFF1D66B3F913D /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; + 2DC17100BA13C8B5325EBD94 /* EEGRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */; }; 2DC1713C311BAA65EE0E2748 /* MuseDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */; }; 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientList.swift */; }; - 2DC171975C15D343ED45A7F3 /* DeviceConnectionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173E2CB87E7470075E022 /* DeviceConnectionListener.swift */; }; 2DC171B41D4A87E2C861C356 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; + 2DC171EFD6619742C29254CE /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */; }; 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; + 2DC172E36CAF9AA8F191F723 /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; 2DC173E694F96E89EAB63FE0 /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; - 2DC17459E46F767C767C0AA0 /* DeviceConnectionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173E2CB87E7470075E022 /* DeviceConnectionListener.swift */; }; + 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; 2DC174CC46386A3F7E20786B /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; - 2DC1752A3985DDF8E7F5866C /* EEGViewModel+ActiveMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1740BAF6AC3F49A133752 /* EEGViewModel+ActiveMock.swift */; }; 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; + 2DC175D2C49F2DCB07A85C1B /* BiopotDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */; }; 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; - 2DC17644A07BC415B89BDACA /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177545B4C12357C3B2613 /* Fit.swift */; }; 2DC17686B3AEB09A8F60AB8E /* PatientList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientList.swift */; }; - 2DC176E7B29173393F43A357 /* MockEEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockEEGDevice.swift */; }; - 2DC1772594424A2D9AF7E666 /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17095E60D83D6205EFA78 /* HeadbandFit.swift */; }; + 2DC176E7B29173393F43A357 /* MockDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockDevice.swift */; }; + 2DC17735CC338B30ECB656B4 /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; + 2DC1776253EA8999F231D6DA /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; + 2DC1776FFC7360126770D201 /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */; }; + 2DC1780ABA3ADC10E942344E /* BiopotDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */; }; + 2DC17877F0B86FDEC613124B /* DeviceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */; }; 2DC17924E7D44FE1A2562528 /* IXNMusePreset+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */; }; 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */; }; - 2DC1795BADBADF4E3409B59E /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1723C364CC7AEF003CC97 /* ConnectionState.swift */; }; 2DC179831EDABDCBD8A1EBFB /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; - 2DC17996ADEFAEC6C12BFB0A /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1723C364CC7AEF003CC97 /* ConnectionState.swift */; }; + 2DC17995C927FACA6C0146B3 /* EEGRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */; }; 2DC179BD6217C86A55D70E6F /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; 2DC179D0C1180EF9B1E8F276 /* MuseDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */; }; - 2DC179F4A6B69C07C1A440D2 /* EEGMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */; }; - 2DC17A1DBD336BC4543CCA81 /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177545B4C12357C3B2613 /* Fit.swift */; }; - 2DC17ABFAC9031FE699864FB /* IXNMuse+EEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */; }; + 2DC179F4A6B69C07C1A440D2 /* MockMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */; }; + 2DC17A3ADA039E3FD901D8CF /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */; }; + 2DC17A843968AEFAB1B64C3B /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; - 2DC17C29AA0382E9F5F2AA4D /* EEGMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */; }; + 2DC17B2C2E86A238A9EB9227 /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; + 2DC17B4DB25C9775EF301CD0 /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; + 2DC17BAFF043EBD5240BDD76 /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */; }; + 2DC17C29AA0382E9F5F2AA4D /* MockMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */; }; 2DC17C341155F06C17225169 /* NewPatientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */; }; + 2DC17C61E1697767983916EF /* DeviceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */; }; 2DC17CA2806C0BFD72984B3D /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; - 2DC17CEE255C54119538D254 /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17095E60D83D6205EFA78 /* HeadbandFit.swift */; }; + 2DC17CAFDF6974EAA57C5D34 /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; + 2DC17CD906FF31BA6EA4CACD /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; - 2DC17D4A9B040AF343D9EF1F /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173D02776C8AAB7386C29 /* BluetoothManager.swift */; }; - 2DC17DA28E7F6C4330CDB7FC /* EEGViewModel+ActiveMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1740BAF6AC3F49A133752 /* EEGViewModel+ActiveMock.swift */; }; - 2DC17DA43107F4E8469B7C73 /* IXNMuse+EEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */; }; + 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; + 2DC17E464BAEACF3A2B554B5 /* MuseTroublesConnectingHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */; }; + 2DC17F033D0282AEE8945A47 /* MuseTroublesConnectingHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */; }; + 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FE89AC206431F90166 /* PatientInformation.swift */; }; - 2DC17F8981E53AEED0CFD1BD /* MockEEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockEEGDevice.swift */; }; + 2DC17F8981E53AEED0CFD1BD /* MockDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockDevice.swift */; }; 2DC17FB80AB25F5356A59FEE /* HeadbandFit+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */; }; 2DC17FE0AC1DD98C29B417F1 /* IXNMusePreset+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */; }; - 2DC17FE79AB13F1F300788A6 /* MuseConnectionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C07ED61540F306E7D23 /* MuseConnectionListener.swift */; }; 2F49B7762980407C00BCB272 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F49B7752980407B00BCB272 /* Spezi */; }; 2F4E237E2989A2FE0013F3D9 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */; }; 2F4E23832989D51F0013F3D9 /* NAMSTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */; }; 2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23862989DB360013F3D9 /* ContactsTests.swift */; }; 2F5E32BD297E05EA003432F8 /* NAMSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */; }; 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; - 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */; }; 2FC3439129EE6349002D773C /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */; }; 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; 2FC975A82978F11A00BA99FE /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; @@ -119,7 +127,6 @@ A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */; }; A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; - A926D7A02AB7A552000C4C2F /* BluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173D02776C8AAB7386C29 /* BluetoothManager.swift */; }; A926D7A82AB7A552000C4C2F /* Muse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A99522432AA61DA6009272F4 /* Muse.framework */; }; A926D7A92AB7A552000C4C2F /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7672AB7A552000C4C2F /* SpeziAccount */; }; A926D7AA2AB7A552000C4C2F /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7692AB7A552000C4C2F /* SpeziContact */; }; @@ -135,7 +142,6 @@ A926D7B82AB7A552000C4C2F /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; A926D7B92AB7A552000C4C2F /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */; }; A926D7BA2AB7A552000C4C2F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; - A926D7BC2AB7A552000C4C2F /* SocialSupportQuestionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */; }; A926D7BD2AB7A552000C4C2F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; A926D7FF2AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F52AB7B41B000C4C2F /* IXNMuseModel+Description.swift */; }; A926D8002AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F52AB7B41B000C4C2F /* IXNMuseModel+Description.swift */; }; @@ -145,12 +151,6 @@ A926D8062AB7B41C000C4C2F /* EEGSeries+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F92AB7B41B000C4C2F /* EEGSeries+Muse.swift */; }; A926D8072AB7B41C000C4C2F /* EEGChannel+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7FA2AB7B41B000C4C2F /* EEGChannel+Muse.swift */; }; A926D8082AB7B41C000C4C2F /* EEGChannel+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7FA2AB7B41B000C4C2F /* EEGChannel+Muse.swift */; }; - A926D81A2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80B2AB7B42F000C4C2F /* EEGDeviceList.swift */; }; - A926D81B2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80B2AB7B42F000C4C2F /* EEGDeviceList.swift */; }; - A926D81C2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80C2AB7B42F000C4C2F /* EEGDeviceRow.swift */; }; - A926D81D2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80C2AB7B42F000C4C2F /* EEGDeviceRow.swift */; }; - A926D81E2AB7B430000C4C2F /* NearbyDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80D2AB7B42F000C4C2F /* NearbyDevices.swift */; }; - A926D81F2AB7B430000C4C2F /* NearbyDevices.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80D2AB7B42F000C4C2F /* NearbyDevices.swift */; }; A926D8202AB7B430000C4C2F /* EEGChannelMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */; }; A926D8212AB7B430000C4C2F /* EEGChannelMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */; }; A926D8222AB7B430000C4C2F /* EEGRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80F2AB7B430000C4C2F /* EEGRecording.swift */; }; @@ -163,14 +163,6 @@ A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8132AB7B430000C4C2F /* EEGReading.swift */; }; A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; A926D82B2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; - A926D82C2AB7B430000C4C2F /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8162AB7B430000C4C2F /* DeviceManager.swift */; }; - A926D82D2AB7B430000C4C2F /* DeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8162AB7B430000C4C2F /* DeviceManager.swift */; }; - A926D82E2AB7B430000C4C2F /* EEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8172AB7B430000C4C2F /* EEGDevice.swift */; }; - A926D82F2AB7B430000C4C2F /* EEGDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8172AB7B430000C4C2F /* EEGDevice.swift */; }; - A926D8302AB7B430000C4C2F /* EEGViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8182AB7B430000C4C2F /* EEGViewModel.swift */; }; - A926D8312AB7B430000C4C2F /* EEGViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8182AB7B430000C4C2F /* EEGViewModel.swift */; }; - A926D8322AB7B430000C4C2F /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8192AB7B430000C4C2F /* ConnectedDevice.swift */; }; - A926D8332AB7B430000C4C2F /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8192AB7B430000C4C2F /* ConnectedDevice.swift */; }; A92DD4D92B02D94F0062781B /* Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92DD4D82B02D94F0062781B /* Biopot.swift */; }; A92DD4DD2B02DA3F0062781B /* Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92DD4D82B02D94F0062781B /* Biopot.swift */; }; A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */; }; @@ -223,8 +215,8 @@ A989112D2A36687B00E66E3A /* PatientListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112C2A36687B00E66E3A /* PatientListSheet.swift */; }; A989112F2A36688A00E66E3A /* PatientRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112E2A36688A00E66E3A /* PatientRow.swift */; }; A98911322A36689D00E66E3A /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; - A9A179532AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* EEGDeviceDetails.swift */; }; - A9A179542AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* EEGDeviceDetails.swift */; }; + A9A179532AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */; }; + A9A179542AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */; }; A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; A9A179572AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; @@ -246,9 +238,15 @@ A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F942B6089C8004703E0 /* BluetoothServices */; }; A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F962B6089D2004703E0 /* BluetoothServices */; }; + A9C82FB72B632EE6004703E0 /* BluetoothStateHints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */; }; + A9C82FB92B633906004703E0 /* LoadingSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */; }; + A9C82FBA2B633906004703E0 /* LoadingSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */; }; + A9C82FBB2B63390E004703E0 /* BluetoothStateHints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */; }; + A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; + A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */; }; - A9CE84512B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */; }; - A9CE84522B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */; }; + A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; + A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9D83F922B081A47000D0C78 /* BiopotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D83F912B081A47000D0C78 /* BiopotTests.swift */; }; A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; @@ -288,31 +286,35 @@ /* Begin PBXFileReference section */ 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseVersion+String.swift"; sourceTree = ""; }; - 2DC17095E60D83D6205EFA78 /* HeadbandFit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadbandFit.swift; sourceTree = ""; }; 2DC170F68232B993528F84FE /* PatientTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientTask.swift; sourceTree = ""; }; - 2DC1723C364CC7AEF003CC97 /* ConnectionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionState.swift; sourceTree = ""; }; + 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadbandFit.swift; sourceTree = ""; }; + 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BiopotDeviceDetailsView.swift; path = Views/BiopotDeviceDetailsView.swift; sourceTree = ""; }; + 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGRecordings.swift; sourceTree = ""; }; + 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCoordinator.swift; sourceTree = ""; }; + 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseTroublesConnectingHint.swift; sourceTree = ""; }; 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FinishedSetup.swift; sourceTree = ""; }; 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewPatientModel.swift; sourceTree = ""; }; - 2DC173D02776C8AAB7386C29 /* BluetoothManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BluetoothManager.swift; path = Bluetooth/BluetoothManager.swift; sourceTree = ""; }; - 2DC173E2CB87E7470075E022 /* DeviceConnectionListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceConnectionListener.swift; sourceTree = ""; }; - 2DC1740BAF6AC3F49A133752 /* EEGViewModel+ActiveMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EEGViewModel+ActiveMock.swift"; sourceTree = ""; }; 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGFrequency.swift; sourceTree = ""; }; 2DC174FE89AC206431F90166 /* PatientInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientInformation.swift; sourceTree = ""; }; 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HeadbandFit+Muse.swift"; sourceTree = ""; }; 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMusePreset+Description.swift"; sourceTree = ""; }; 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDeviceManager.swift; sourceTree = ""; }; - 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuse+EEGDevice.swift"; sourceTree = ""; }; - 2DC177545B4C12357C3B2613 /* Fit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fit.swift; sourceTree = ""; }; 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionState+Muse.swift"; sourceTree = ""; }; 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiopotDevicePreview.swift; sourceTree = ""; }; 2DC177BFA6C401C2C87FCD5C /* TileType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TileType.swift; sourceTree = ""; }; + 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fit.swift; sourceTree = ""; }; 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseConfiguration+Description.swift"; sourceTree = ""; }; + 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionState.swift; sourceTree = ""; }; + 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BiopotDeviceRow.swift; path = Views/BiopotDeviceRow.swift; sourceTree = ""; }; + 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockDeviceRow.swift; path = Views/MockDeviceRow.swift; sourceTree = ""; }; + 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceList.swift; sourceTree = ""; }; 2DC17A06799FC1470D4DDC0D /* PatientList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientList.swift; sourceTree = ""; }; - 2DC17C07ED61540F306E7D23 /* MuseConnectionListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseConnectionListener.swift; sourceTree = ""; }; + 2DC17C8A120ECD3394366958 /* MuseDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDevice.swift; sourceTree = ""; }; 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseDataPacketType+Type.swift"; sourceTree = ""; }; - 2DC17D172D8299600ED13D60 /* MockEEGDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEEGDevice.swift; sourceTree = ""; }; - 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGMeasurementGenerator.swift; sourceTree = ""; }; + 2DC17D172D8299600ED13D60 /* MockDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDevice.swift; sourceTree = ""; }; + 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMeasurementGenerator.swift; sourceTree = ""; }; 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceManager.swift; sourceTree = ""; }; + 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceRow.swift; sourceTree = ""; }; 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAMSTestingSetup.swift; sourceTree = ""; }; 2F4E23862989DB360013F3D9 /* ContactsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsTests.swift; sourceTree = ""; }; @@ -331,7 +333,6 @@ 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+Negate.swift"; sourceTree = ""; }; 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Image.swift"; sourceTree = ""; }; 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CodableArray+RawRepresentable.swift"; sourceTree = ""; }; - 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SocialSupportQuestionnaire.json; sourceTree = ""; }; 653A254D283387FE005D4D48 /* NAMS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NAMS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 653A2550283387FE005D4D48 /* NAMSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAMSApp.swift; sourceTree = ""; }; 653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -354,19 +355,12 @@ A926D7F82AB7B41B000C4C2F /* EEGReading+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EEGReading+Muse.swift"; sourceTree = ""; }; A926D7F92AB7B41B000C4C2F /* EEGSeries+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EEGSeries+Muse.swift"; sourceTree = ""; }; A926D7FA2AB7B41B000C4C2F /* EEGChannel+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EEGChannel+Muse.swift"; sourceTree = ""; }; - A926D80B2AB7B42F000C4C2F /* EEGDeviceList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGDeviceList.swift; sourceTree = ""; }; - A926D80C2AB7B42F000C4C2F /* EEGDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGDeviceRow.swift; sourceTree = ""; }; - A926D80D2AB7B42F000C4C2F /* NearbyDevices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NearbyDevices.swift; sourceTree = ""; }; A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChannelMark.swift; sourceTree = ""; }; A926D80F2AB7B430000C4C2F /* EEGRecording.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGRecording.swift; sourceTree = ""; }; A926D8112AB7B430000C4C2F /* EEGSeries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGSeries.swift; sourceTree = ""; }; A926D8122AB7B430000C4C2F /* EEGChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChannel.swift; sourceTree = ""; }; A926D8132AB7B430000C4C2F /* EEGReading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGReading.swift; sourceTree = ""; }; A926D8142AB7B430000C4C2F /* EEGChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChart.swift; sourceTree = ""; }; - A926D8162AB7B430000C4C2F /* DeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceManager.swift; sourceTree = ""; }; - A926D8172AB7B430000C4C2F /* EEGDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGDevice.swift; sourceTree = ""; }; - A926D8182AB7B430000C4C2F /* EEGViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGViewModel.swift; sourceTree = ""; }; - A926D8192AB7B430000C4C2F /* ConnectedDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; A92DD4D82B02D94F0062781B /* Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Biopot.swift; sourceTree = ""; }; A9405B552A36856300C75412 /* AddPatientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPatientView.swift; sourceTree = ""; }; A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTile.swift; sourceTree = ""; }; @@ -394,7 +388,7 @@ A98911312A36689D00E66E3A /* Patient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patient.swift; sourceTree = ""; }; A99522432AA61DA6009272F4 /* Muse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Muse.framework; sourceTree = ""; }; A99522462AA61FE5009272F4 /* NAMS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NAMS-Bridging-Header.h"; sourceTree = ""; }; - A9A179522AC6266A00B180D8 /* EEGDeviceDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGDeviceDetails.swift; sourceTree = ""; }; + A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuseDeviceDetailsView.swift; sourceTree = ""; }; A9A179552AC62BE500B180D8 /* ListRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientSearchModel.swift; sourceTree = ""; }; @@ -404,8 +398,11 @@ A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentPatientLabel.swift; sourceTree = ""; }; A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoInformationText.swift; sourceTree = ""; }; A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImpedanceMeasurement.swift; sourceTree = ""; }; + A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateHints.swift; sourceTree = ""; }; + A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSectionHeader.swift; sourceTree = ""; }; + A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceRow.swift; sourceTree = ""; }; A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGDeviceTests.swift; sourceTree = ""; }; - A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesSheet.swift; sourceTree = ""; }; + A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPatientCard.swift; sourceTree = ""; }; A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTile.swift; sourceTree = ""; }; @@ -520,7 +517,6 @@ A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */, 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */, 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */, - 2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */, ); path = Resources; sourceTree = ""; @@ -564,7 +560,6 @@ 653A254F283387FE005D4D48 /* NAMS */ = { isa = PBXGroup; children = ( - 2DC173D02776C8AAB7386C29 /* BluetoothManager.swift */, 2FC975A72978F11A00BA99FE /* Home.swift */, 653A2550283387FE005D4D48 /* NAMSApp.swift */, 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */, @@ -619,6 +614,7 @@ A967061A2B1AA28100C17BE5 /* Chart */, A926D8102AB7B430000C4C2F /* Recording */, A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */, + 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */, ); path = EEG; sourceTree = ""; @@ -650,6 +646,9 @@ 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */, 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */, 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */, + 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */, + 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */, + 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */, ); path = Model; sourceTree = ""; @@ -665,28 +664,13 @@ path = Recording; sourceTree = ""; }; - A926D8152AB7B430000C4C2F /* Model */ = { - isa = PBXGroup; - children = ( - A926D8162AB7B430000C4C2F /* DeviceManager.swift */, - A926D8172AB7B430000C4C2F /* EEGDevice.swift */, - A926D8182AB7B430000C4C2F /* EEGViewModel.swift */, - A926D8192AB7B430000C4C2F /* ConnectedDevice.swift */, - 2DC1723C364CC7AEF003CC97 /* ConnectionState.swift */, - 2DC173E2CB87E7470075E022 /* DeviceConnectionListener.swift */, - 2DC177545B4C12357C3B2613 /* Fit.swift */, - 2DC17095E60D83D6205EFA78 /* HeadbandFit.swift */, - ); - path = Model; - sourceTree = ""; - }; A926D8342AB7C2CC000C4C2F /* Mock */ = { isa = PBXGroup; children = ( 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */, - 2DC17D172D8299600ED13D60 /* MockEEGDevice.swift */, - 2DC1740BAF6AC3F49A133752 /* EEGViewModel+ActiveMock.swift */, - 2DC17D48217697F558657E69 /* EEGMeasurementGenerator.swift */, + 2DC17D172D8299600ED13D60 /* MockDevice.swift */, + 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */, + 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */, ); path = Mock; sourceTree = ""; @@ -757,14 +741,16 @@ isa = PBXGroup; children = ( A97E4F212B1EA21000E25505 /* Recording */, - A988FEB02B0452AB00022A61 /* Model */, + A988FEB02B0452AB00022A61 /* Characteristics */, A92DD4D82B02D94F0062781B /* Biopot.swift */, A988FEA92B0414FD00022A61 /* BiopotDevice.swift */, + 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */, + 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */, ); path = BioPot; sourceTree = ""; }; - A988FEB02B0452AB00022A61 /* Model */ = { + A988FEB02B0452AB00022A61 /* Characteristics */ = { isa = PBXGroup; children = ( A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */, @@ -777,7 +763,7 @@ A907DA3E2B1964B500FB69FB /* ByteBuffer+Int24.swift */, A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */, ); - path = Model; + path = Characteristics; sourceTree = ""; }; A989112B2A36686D00E66E3A /* Patients */ = { @@ -815,9 +801,9 @@ A926D7F32AB7B41B000C4C2F /* Extensions */, A926D7FB2AB7B41C000C4C2F /* Model */, A926D7F72AB7B41B000C4C2F /* Recording */, + A9EB34D02B64DAB000FD62C3 /* Views */, + 2DC17C8A120ECD3394366958 /* MuseDevice.swift */, 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */, - 2DC1768C714141C636C5CFCD /* IXNMuse+EEGDevice.swift */, - 2DC17C07ED61540F306E7D23 /* MuseConnectionListener.swift */, ); path = Muse; sourceTree = ""; @@ -835,19 +821,26 @@ path = Utils; sourceTree = ""; }; + A9C82FB52B632EC3004703E0 /* MaybeExternal */ = { + isa = PBXGroup; + children = ( + A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */, + A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */, + A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */, + ); + path = MaybeExternal; + sourceTree = ""; + }; A9CE844F2B1A99FE009CE3F4 /* Devices */ = { isa = PBXGroup; children = ( + A9C82FB52B632EC3004703E0 /* MaybeExternal */, A988FEAE2B04529B00022A61 /* BioPot */, A926D8342AB7C2CC000C4C2F /* Mock */, - A926D8152AB7B430000C4C2F /* Model */, A99522402AA61D82009272F4 /* Muse */, A988FEAB2B043AED00022A61 /* BatteryIcon.swift */, - A9CE84502B1A9A14009CE3F4 /* DevicesSheet.swift */, - A9A179522AC6266A00B180D8 /* EEGDeviceDetails.swift */, - A926D80B2AB7B42F000C4C2F /* EEGDeviceList.swift */, - A926D80C2AB7B42F000C4C2F /* EEGDeviceRow.swift */, - A926D80D2AB7B42F000C4C2F /* NearbyDevices.swift */, + A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */, + 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */, ); path = Devices; sourceTree = ""; @@ -861,6 +854,17 @@ path = Testing; sourceTree = ""; }; + A9EB34D02B64DAB000FD62C3 /* Views */ = { + isa = PBXGroup; + children = ( + A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */, + 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */, + 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */, + 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1042,7 +1046,6 @@ 2FC3439229EE634B002D773C /* ConsentDocument.md in Resources */, 2FC3439129EE6349002D773C /* AppIcon.png in Resources */, 653A255528338800005D4D48 /* Assets.xcassets in Resources */, - 2FC3439029EE6346002D773C /* SocialSupportQuestionnaire.json in Resources */, A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */, A94534062AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, @@ -1070,7 +1073,6 @@ A926D7B82AB7A552000C4C2F /* ConsentDocument.md in Resources */, A926D7B92AB7A552000C4C2F /* AppIcon.png in Resources */, A926D7BA2AB7A552000C4C2F /* Assets.xcassets in Resources */, - A926D7BC2AB7A552000C4C2F /* SocialSupportQuestionnaire.json in Resources */, A9BCB57D2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, A926D7BD2AB7A552000C4C2F /* GoogleService-Info.plist in Resources */, A94534072AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, @@ -1126,7 +1128,6 @@ A907DA3C2B195ED800FB69FB /* EEGSample.swift in Sources */, A94534002AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, A9C82F922B608756004703E0 /* ImpedanceMeasurement.swift in Sources */, - A926D8302AB7B430000C4C2F /* EEGViewModel.swift in Sources */, A94A42AE2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, A926D7FF2AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, 2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */, @@ -1137,9 +1138,8 @@ A926D8222AB7B430000C4C2F /* EEGRecording.swift in Sources */, A916ADD52AB60227006960DF /* NotificationPermissions.swift in Sources */, A907DA302B192FD500FB69FB /* DataControl.swift in Sources */, - A9A179532AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */, + A9A179532AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */, A94533FA2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */, - A926D81E2AB7B430000C4C2F /* NearbyDevices.swift in Sources */, A9BCB58C2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */, @@ -1148,6 +1148,7 @@ A94A42B62AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */, A9F2ECC62AEB27B10057C7DD /* MeasurementTile.swift in Sources */, + A9C82FB72B632EE6004703E0 /* BluetoothStateHints.swift in Sources */, A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */, A926D8202AB7B430000C4C2F /* EEGChannelMark.swift in Sources */, A916ADD92AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift in Sources */, @@ -1156,17 +1157,14 @@ A94A42B22AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, A92DD4D92B02D94F0062781B /* Biopot.swift in Sources */, A926D8242AB7B430000C4C2F /* EEGSeries.swift in Sources */, - A926D81A2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */, A97E4F232B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, - A926D82C2AB7B430000C4C2F /* DeviceManager.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */, A907DA362B1942B800FB69FB /* DataAcquisition.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, - A926D8322AB7B430000C4C2F /* ConnectedDevice.swift in Sources */, A988FEB52B0453E100022A61 /* DeviceInformation.swift in Sources */, A9F2ECD02AEC5EF50057C7DD /* MeasurementTask.swift in Sources */, A9BCB5862AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift in Sources */, @@ -1175,7 +1173,6 @@ A907DA332B193C9700FB69FB /* SamplingConfiguration.swift in Sources */, 2F5E32BD297E05EA003432F8 /* NAMSAppDelegate.swift in Sources */, A94534182AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */, - A926D81C2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */, 653A2551283387FE005D4D48 /* NAMSApp.swift in Sources */, A989112F2A36688A00E66E3A /* PatientRow.swift in Sources */, A98911322A36689D00E66E3A /* Patient.swift in Sources */, @@ -1184,16 +1181,12 @@ A988FEAC2B043AED00022A61 /* BatteryIcon.swift in Sources */, 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */, A988FEAA2B0414FD00022A61 /* BiopotDevice.swift in Sources */, - A926D82E2AB7B430000C4C2F /* EEGDevice.swift in Sources */, A9BCB5892AE83F7E00DA8588 /* PatientListModel.swift in Sources */, A926D8262AB7B430000C4C2F /* EEGChannel.swift in Sources */, A926D8032AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, - 2DC17D4A9B040AF343D9EF1F /* BluetoothManager.swift in Sources */, A926D8282AB7B430000C4C2F /* EEGReading.swift in Sources */, - 2DC17996ADEFAEC6C12BFB0A /* ConnectionState.swift in Sources */, A9BCB57F2AE82FFC00DA8588 /* PatientSearchModel.swift in Sources */, 2DC171B41D4A87E2C861C356 /* ConnectionState+Muse.swift in Sources */, - 2DC171975C15D343ED45A7F3 /* DeviceConnectionListener.swift in Sources */, 2DC17CA2806C0BFD72984B3D /* MockDeviceManager.swift in Sources */, A967061C2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */, 2DC174CC46386A3F7E20786B /* IXNMuseVersion+String.swift in Sources */, @@ -1204,28 +1197,37 @@ 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */, 2DC179D0C1180EF9B1E8F276 /* MuseDeviceManager.swift in Sources */, A94534132AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, - 2DC17ABFAC9031FE699864FB /* IXNMuse+EEGDevice.swift in Sources */, 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */, A945340C2AEAE6380095AAD3 /* TilesView.swift in Sources */, A907DA392B195D4800FB69FB /* AccelerometerSample.swift in Sources */, A9F2ECCD2AEC58B00057C7DD /* SimpleTile.swift in Sources */, - 2DC17FE79AB13F1F300788A6 /* MuseConnectionListener.swift in Sources */, - 2DC17A1DBD336BC4543CCA81 /* Fit.swift in Sources */, 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */, A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */, - 2DC1772594424A2D9AF7E666 /* HeadbandFit.swift in Sources */, A907DA3F2B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, - A9CE84512B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */, - 2DC17F8981E53AEED0CFD1BD /* MockEEGDevice.swift in Sources */, - 2DC1752A3985DDF8E7F5866C /* EEGViewModel+ActiveMock.swift in Sources */, - 2DC17C29AA0382E9F5F2AA4D /* EEGMeasurementGenerator.swift in Sources */, + A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, + 2DC17F8981E53AEED0CFD1BD /* MockDevice.swift in Sources */, + 2DC17C29AA0382E9F5F2AA4D /* MockMeasurementGenerator.swift in Sources */, 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */, 2DC17C341155F06C17225169 /* NewPatientModel.swift in Sources */, + A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */, + A9C82FB92B633906004703E0 /* LoadingSectionHeader.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */, 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */, 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */, + 2DC1776253EA8999F231D6DA /* MuseDevice.swift in Sources */, + 2DC17E464BAEACF3A2B554B5 /* MuseTroublesConnectingHint.swift in Sources */, + 2DC17C61E1697767983916EF /* DeviceCoordinator.swift in Sources */, + 2DC17735CC338B30ECB656B4 /* MuseDeviceRow.swift in Sources */, + 2DC17CAFDF6974EAA57C5D34 /* MuseDeviceList.swift in Sources */, + 2DC172E36CAF9AA8F191F723 /* MockDeviceRow.swift in Sources */, + 2DC175D2C49F2DCB07A85C1B /* BiopotDeviceRow.swift in Sources */, + 2DC17B2C2E86A238A9EB9227 /* BiopotDeviceDetailsView.swift in Sources */, + 2DC17100BA13C8B5325EBD94 /* EEGRecordings.swift in Sources */, + 2DC17A3ADA039E3FD901D8CF /* HeadbandFit.swift in Sources */, + 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */, + 2DC171EFD6619742C29254CE /* Fit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1256,9 +1258,10 @@ buildActionMask = 2147483647; files = ( A907DA3D2B195ED800FB69FB /* EEGSample.swift in Sources */, + A9C82FBA2B633906004703E0 /* LoadingSectionHeader.swift in Sources */, A94534012AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, - A926D8312AB7B430000C4C2F /* EEGViewModel.swift in Sources */, A94A42AF2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, + A9C82FBB2B63390E004703E0 /* BluetoothStateHints.swift in Sources */, A926D8002AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, A9BCB5872AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift in Sources */, A926D77F2AB7A552000C4C2F /* StorageKeys.swift in Sources */, @@ -1271,8 +1274,7 @@ A9DF79E02AE8A82100AB5983 /* PatientListSheet.swift in Sources */, A94533FB2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */, A926D7822AB7A552000C4C2F /* NotificationPermissions.swift in Sources */, - A9A179542AC6266A00B180D8 /* EEGDeviceDetails.swift in Sources */, - A926D81F2AB7B430000C4C2F /* NearbyDevices.swift in Sources */, + A9A179542AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */, A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */, A926D7872AB7A552000C4C2F /* Welcome.swift in Sources */, A926D7882AB7A552000C4C2F /* Binding+Negate.swift in Sources */, @@ -1289,12 +1291,10 @@ A926D8252AB7B430000C4C2F /* EEGSeries.swift in Sources */, A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */, A97E4F242B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */, - A926D81B2AB7B430000C4C2F /* EEGDeviceList.swift in Sources */, A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, A926D78E2AB7A552000C4C2F /* OnboardingFlow.swift in Sources */, A9BCB5842AE830FB00DA8588 /* NewPatientModel.swift in Sources */, A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */, - A926D82D2AB7B430000C4C2F /* DeviceManager.swift in Sources */, A907DA372B1942B800FB69FB /* DataAcquisition.swift in Sources */, A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */, A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */, @@ -1303,7 +1303,6 @@ A9F2ECD12AEC5EF50057C7DD /* MeasurementTask.swift in Sources */, A9BCB5832AE8307800DA8588 /* SearchToken.swift in Sources */, A926D7932AB7A552000C4C2F /* Bundle+Image.swift in Sources */, - A926D8332AB7B430000C4C2F /* ConnectedDevice.swift in Sources */, A907DA342B193C9700FB69FB /* SamplingConfiguration.swift in Sources */, A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, A94534192AEB0DB20095AAD3 /* QuestionnaireError.swift in Sources */, @@ -1313,20 +1312,15 @@ A926D79A2AB7A552000C4C2F /* NAMSAppDelegate.swift in Sources */, A9DF79E22AE8A82600AB5983 /* PatientInformation.swift in Sources */, A94534112AEAF2AE0095AAD3 /* CompletedTask.swift in Sources */, - A926D81D2AB7B430000C4C2F /* EEGDeviceRow.swift in Sources */, A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */, A988FEAF2B0452A900022A61 /* BiopotDevice.swift in Sources */, A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */, A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */, - A926D82F2AB7B430000C4C2F /* EEGDevice.swift in Sources */, A926D8272AB7B430000C4C2F /* EEGChannel.swift in Sources */, A926D8042AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, - A926D7A02AB7A552000C4C2F /* BluetoothManager.swift in Sources */, A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */, A988FEAD2B043AED00022A61 /* BatteryIcon.swift in Sources */, - 2DC1795BADBADF4E3409B59E /* ConnectionState.swift in Sources */, 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */, - 2DC17459E46F767C767C0AA0 /* DeviceConnectionListener.swift in Sources */, A967061D2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */, 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */, 2DC179831EDABDCBD8A1EBFB /* IXNMuseVersion+String.swift in Sources */, @@ -1337,27 +1331,35 @@ 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */, A94534142AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC1713C311BAA65EE0E2748 /* MuseDeviceManager.swift in Sources */, - 2DC17DA43107F4E8469B7C73 /* IXNMuse+EEGDevice.swift in Sources */, A945340D2AEAE6380095AAD3 /* TilesView.swift in Sources */, A907DA3A2B195D4800FB69FB /* AccelerometerSample.swift in Sources */, A9F2ECCE2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */, - 2DC17128D81F6CF06F65FFD4 /* MuseConnectionListener.swift in Sources */, - 2DC17644A07BC415B89BDACA /* Fit.swift in Sources */, A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */, 2DC17FB80AB25F5356A59FEE /* HeadbandFit+Muse.swift in Sources */, A907DA402B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, - A9CE84522B1A9A14009CE3F4 /* DevicesSheet.swift in Sources */, + A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, A9DF79E12AE8A82300AB5983 /* PatientRow.swift in Sources */, - 2DC17CEE255C54119538D254 /* HeadbandFit.swift in Sources */, - 2DC176E7B29173393F43A357 /* MockEEGDevice.swift in Sources */, - 2DC17DA28E7F6C4330CDB7FC /* EEGViewModel+ActiveMock.swift in Sources */, - 2DC179F4A6B69C07C1A440D2 /* EEGMeasurementGenerator.swift in Sources */, + 2DC176E7B29173393F43A357 /* MockDevice.swift in Sources */, + A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, + 2DC179F4A6B69C07C1A440D2 /* MockMeasurementGenerator.swift in Sources */, A9F2ECCA2AEC2C300057C7DD /* CompletedTile.swift in Sources */, 2DC17686B3AEB09A8F60AB8E /* PatientList.swift in Sources */, 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */, 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */, 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */, + 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */, + 2DC17F033D0282AEE8945A47 /* MuseTroublesConnectingHint.swift in Sources */, + 2DC17877F0B86FDEC613124B /* DeviceCoordinator.swift in Sources */, + 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */, + 2DC17A843968AEFAB1B64C3B /* MuseDeviceList.swift in Sources */, + 2DC17CD906FF31BA6EA4CACD /* MockDeviceRow.swift in Sources */, + 2DC1780ABA3ADC10E942344E /* BiopotDeviceRow.swift in Sources */, + 2DC17B4DB25C9775EF301CD0 /* BiopotDeviceDetailsView.swift in Sources */, + 2DC17995C927FACA6C0146B3 /* EEGRecordings.swift in Sources */, + 2DC1776FFC7360126770D201 /* HeadbandFit.swift in Sources */, + 2DC1707B04BFF1D66B3F913D /* ConnectionState.swift in Sources */, + 2DC17BAFF043EBD5240BDD76 /* Fit.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2d836d7..ddccf0a 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "115f75e43851774934d695449a4836123c3246e1", - "version" : "3.2.0" + "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", + "version" : "3.3.1" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKit", "state" : { - "revision" : "cf79a15c7d8c436f98937fe93e72e880dd2f73e4", - "version" : "2.2.20" + "revision" : "209164ed20592a2213c4bd69cefcb078d9de0692", + "version" : "2.2.21" } }, { @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", "state" : { - "revision" : "7dc09f7acd7fb19673594e0fdd4d72d0869ee006", - "version" : "1.0.0" + "revision" : "ea4d9691591594177e7dfbc8c246324855d73eb5", + "version" : "1.0.1" } }, { @@ -159,7 +159,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { "branch" : "feature/unit-testing-setup", - "revision" : "f9db62d3b53475b52cc6f56ed92242482dd4f72d" + "revision" : "e6e1b009b00463d39bfa99d3b491fa3b1b562098" } }, { @@ -203,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", "state" : { - "revision" : "930a4099db1aca9db0b6ed4e77687141c4780052", - "version" : "1.0.0" + "revision" : "fac0bb02f7027b4c09bd7afdad55eb7b47ec67f3", + "version" : "1.0.1" } }, { diff --git a/NAMS/Bluetooth/BluetoothManager.swift b/NAMS/Bluetooth/BluetoothManager.swift deleted file mode 100644 index 1a37c94..0000000 --- a/NAMS/Bluetooth/BluetoothManager.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import CoreBluetooth -import OSLog - - -@Observable -class BluetoothManager: NSObject, CBCentralManagerDelegate { - private let logger = Logger(subsystem: "edu.standford.nams", category: "BluetoothManager") - - private let bluetoothManager: CBCentralManager - private let dispatchQueue: DispatchQueue - - private(set) var bluetoothState: CBManagerState - - override init() { - // We use a separate dispatch queue, be aware that all delegate calls are not getting on the main thread. - self.dispatchQueue = DispatchQueue(label: "CBCentralManager") - self.bluetoothManager = CBCentralManager(delegate: nil, queue: dispatchQueue) - self.bluetoothState = bluetoothManager.state - - super.init() - - // CBCentralManager declares the delegate as weak - self.bluetoothManager.delegate = self // cannot use self before super.init() - } - - func centralManagerDidUpdateState(_ central: CBCentralManager) { - Task { @MainActor in - self.bluetoothState = central.state - logger.debug("BluetoothState is now \(central.state.rawValue)") - } - } -} diff --git a/NAMS/Devices/BatteryIcon.swift b/NAMS/Devices/BatteryIcon.swift index e1f1389..f36c028 100644 --- a/NAMS/Devices/BatteryIcon.swift +++ b/NAMS/Devices/BatteryIcon.swift @@ -10,22 +10,30 @@ import SwiftUI struct BatteryIcon: View { private let percentage: Int + private let isCharging: Bool + var body: some View { - Text(verbatim: "\(percentage) %") - batteryIcon(percentage: percentage) // hides accessibility, only text will be shown - .foregroundStyle(.primary) + HStack { + Text(verbatim: "\(percentage) %") + batteryIcon // hides accessibility, only text will be shown + .foregroundStyle(.primary) + } + .accessibilityRepresentation { + if !isCharging { + Text(verbatim: "\(percentage) %") + } else { + Text(verbatim: "\(percentage) %, is charging") + } + } } - init(percentage: Int) { - self.percentage = percentage - } - - @ViewBuilder - func batteryIcon(percentage: Int) -> some View { + @ViewBuilder var batteryIcon: some View { Group { - if percentage >= 90 { + if isCharging { + Image(systemName: "battery.100percent.bolt") + } else if percentage >= 90 { Image(systemName: "battery.100") } else if percentage >= 65 { Image(systemName: "battery.75") @@ -42,7 +50,18 @@ struct BatteryIcon: View { .foregroundColor(.red) } } - .accessibilityHidden(true) + .accessibilityHidden(true) + } + + + init(percentage: Int, isCharging: Bool) { + self.percentage = percentage + self.isCharging = isCharging + } + + init(percentage: Int) { + // isCharging=false is the same behavior as having no charging information + self.init(percentage: percentage, isCharging: false) } } @@ -52,6 +71,10 @@ struct BatteryIcon: View { BatteryIcon(percentage: 100) } +#Preview { + BatteryIcon(percentage: 85, isCharging: true) +} + #Preview { BatteryIcon(percentage: 70) } diff --git a/NAMS/Devices/BioPot/BiopotDevice.swift b/NAMS/Devices/BioPot/BiopotDevice.swift index 1223211..ffca37b 100644 --- a/NAMS/Devices/BioPot/BiopotDevice.swift +++ b/NAMS/Devices/BioPot/BiopotDevice.swift @@ -12,7 +12,6 @@ import OSLog import Spezi import SpeziBluetooth import class CoreBluetooth.CBUUID -import SwiftUI class BiopotService: BluetoothService { @@ -51,7 +50,12 @@ class BiopotDevice: BluetoothDevice, Identifiable { @DeviceState(\.name) var name - var connected: Bool { + @DeviceAction(\.connect) + var connect + @DeviceAction(\.disconnect) + var disconnect + + var connected: Bool { // TODO: remove? state == .connected } @@ -61,19 +65,13 @@ class BiopotDevice: BluetoothDevice, Identifiable { @Service(id: .biopotService) var service = BiopotService() - @MainActor var startDate: Date? - - @Binding private var recordingSession: EEGRecordingSession? + @MainActor private var recordingSession: EEGRecordingSession? + @MainActor private var startDate: Date? required init() { - self._recordingSession = .constant(nil) - service.$dataAcquisition .onChange(perform: handleDataAcquisition) - } - - func associate(_ model: EEGViewModel) { // TODO: handle this everytime it gets newly created? - self._recordingSession = model.recordingSessionBinding + $state.onChange(perform: handleChange) } private func handleChange(of state: PeripheralState) { @@ -90,21 +88,33 @@ class BiopotDevice: BluetoothDevice, Identifiable { } } - func enableRecording() async { + @MainActor + func startRecording(_ session: EEGRecordingSession) async throws { + recordingSession = session + try await self.enableRecording() + } + + @MainActor + func stopRecording() { + recordingSession = nil + // TODO: async operation to stop data collection + } + + @MainActor + func enableRecording() async throws { do { try await service.$dataControl.write(false) // make sure the value is up to date _ = try await service.$deviceConfiguration.read() - await MainActor.run { - startDate = .now - } + startDate = .now try await service.$dataControl.write(true) _ = try await service.$samplingConfiguration.read() } catch { logger.error("Failed to enable Biopot recording: \(error)") + throw error } } @@ -157,7 +167,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { return EEGSeries(timestamp: timestamp, readings: readings) } - recordingSession.measurements[.all, default: []].append(contentsOf: series) + recordingSession.append(series: series, for: .all) } } } @@ -174,6 +184,16 @@ extension BiopotDevice: Hashable { } +extension BiopotDevice: GenericBluetoothPeripheral { + var label: String { + // TODO: default naming? + name ?? "unknown device" // TODO: maybe strip out the mac address? + } + // TODO: have dedicated accessibility label? +} + + +// TODO: move that extension CBUUID { static let biopotService = CBUUID(string: "FFF0") diff --git a/NAMS/Devices/BioPot/Model/AccelerometerSample.swift b/NAMS/Devices/BioPot/Characteristics/AccelerometerSample.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/AccelerometerSample.swift rename to NAMS/Devices/BioPot/Characteristics/AccelerometerSample.swift diff --git a/NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift b/NAMS/Devices/BioPot/Characteristics/ByteBuffer+Int24.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/ByteBuffer+Int24.swift rename to NAMS/Devices/BioPot/Characteristics/ByteBuffer+Int24.swift diff --git a/NAMS/Devices/BioPot/Model/DataAcquisition.swift b/NAMS/Devices/BioPot/Characteristics/DataAcquisition.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/DataAcquisition.swift rename to NAMS/Devices/BioPot/Characteristics/DataAcquisition.swift diff --git a/NAMS/Devices/BioPot/Model/DataControl.swift b/NAMS/Devices/BioPot/Characteristics/DataControl.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/DataControl.swift rename to NAMS/Devices/BioPot/Characteristics/DataControl.swift diff --git a/NAMS/Devices/BioPot/Model/DeviceConfiguration.swift b/NAMS/Devices/BioPot/Characteristics/DeviceConfiguration.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/DeviceConfiguration.swift rename to NAMS/Devices/BioPot/Characteristics/DeviceConfiguration.swift diff --git a/NAMS/Devices/BioPot/Model/DeviceInformation.swift b/NAMS/Devices/BioPot/Characteristics/DeviceInformation.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/DeviceInformation.swift rename to NAMS/Devices/BioPot/Characteristics/DeviceInformation.swift diff --git a/NAMS/Devices/BioPot/Model/EEGSample.swift b/NAMS/Devices/BioPot/Characteristics/EEGSample.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/EEGSample.swift rename to NAMS/Devices/BioPot/Characteristics/EEGSample.swift diff --git a/NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift b/NAMS/Devices/BioPot/Characteristics/ImpedanceMeasurement.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/ImpedanceMeasurement.swift rename to NAMS/Devices/BioPot/Characteristics/ImpedanceMeasurement.swift diff --git a/NAMS/Devices/BioPot/Model/SamplingConfiguration.swift b/NAMS/Devices/BioPot/Characteristics/SamplingConfiguration.swift similarity index 100% rename from NAMS/Devices/BioPot/Model/SamplingConfiguration.swift rename to NAMS/Devices/BioPot/Characteristics/SamplingConfiguration.swift diff --git a/NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift b/NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift new file mode 100644 index 0000000..57a992e --- /dev/null +++ b/NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct BiopotDeviceDetailsView: View { + private let biopot: BiopotDevice + private let disconnectClosure: () -> Void + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + List { + if let info = biopot.service.deviceInfo { + Section { + ListRow("BATTERY") { + BatteryIcon(percentage: Int(info.batteryLevel), isCharging: info.batteryCharging) + } + } + } + + Section("About") { + if let firmware = biopot.deviceInformation.firmwareRevision { + ListRow("FIRMWARE_VERSION") { + Text(verbatim: firmware) + } + } + if let hardware = biopot.deviceInformation.hardwareRevision { + ListRow("Hardware Version") { + Text(verbatim: hardware) + } + } + if let serialNumber = biopot.deviceInformation.serialNumber { + ListRow("SERIAL_NUMBER") { + Text(verbatim: serialNumber) + } + } + } + + Button(action: { + disconnectClosure() + dismiss() + }) { + Text("DISCONNECT") + .frame(maxWidth: .infinity) + } + // TODO: .disabled(!state.associatedConnection) + } + .navigationTitle(Text(verbatim: biopot.name!)) // TODO: avoid! + .navigationBarTitleDisplayMode(.inline) + } + + + init(device: BiopotDevice, disconnect: @escaping () -> Void) { + self.biopot = device + self.disconnectClosure = disconnect + } +} + + +// TODO: preview diff --git a/NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift b/NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift new file mode 100644 index 0000000..90cbda6 --- /dev/null +++ b/NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct BiopotDeviceRow: View { + private let device: BiopotDevice + + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + + @State private var presentingActiveDevice: BiopotDevice? + + + var body: some View { + NearbyDeviceRow(peripheral: device) { + Task { + await deviceCoordinator.tapDevice(.biopot(device)) + } + } secondaryAction: { + // TODO: we assume I button only shows if this is true! + if device.state == .connected { + presentingActiveDevice = device + } + } + .navigationDestination(item: $presentingActiveDevice) { device in + BiopotDeviceDetailsView(device: device) { + Task { + await device.disconnect() + deviceCoordinator.hintDisconnect() + } + } + } + } + + init(device: BiopotDevice) { + self.device = device + } +} + + +// TODO: preview diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift new file mode 100644 index 0000000..660e2ea --- /dev/null +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -0,0 +1,154 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OSLog +import Spezi +import SpeziBluetooth + +enum SomeDevice { // TODO: move to separate file +#if MUSE + case muse(_ muse: MuseDevice) +#endif + case biopot(_ biopot: BiopotDevice) + case mock(_ mock: MockDevice) + + func connect() async { + switch self { +#if MUSE + case let .muse(muse): + muse.connect() +#endif + case let .biopot(biopot): + await biopot.connect() + case let .mock(mock): + mock.connect() + } + } + + func disconnect() async { + switch self { +#if MUSE + case let .muse(muse): + muse.disconnect() +#endif + case let .biopot(biopot): + await biopot.disconnect() + case let .mock(mock): + mock.disconnect() + } + } + + @MainActor + func startRecording(_ session: EEGRecordingSession) async throws { + switch self { + #if MUSE + case let .muse(muse): + try await muse.startRecording(session) + #endif + case let .biopot(biopot): + try await biopot.startRecording(session) + case let .mock(mock): + try await mock.startRecording(session) + } + } + + @MainActor + func stopRecording() { + switch self { + #if MUSE + case let .muse(muse): + muse.stopRecording() + #endif + case let .biopot(biopot): + biopot.stopRecording() + case let .mock(mock): + mock.stopRecording() + } + } +} + +extension SomeDevice: Hashable {} + + +extension SomeDevice: GenericBluetoothPeripheral { + var label: String { + switch self { +#if MUSE + case let .muse(muse): + muse.label +#endif + case let .biopot(biopot): + biopot.label + case let .mock(mock): + mock.label + } + } + + var state: SpeziBluetooth.PeripheralState { + switch self { +#if MUSE + case let .muse(muse): + muse.state +#endif + case let .biopot(biopot): + biopot.state + case let .mock(mock): + mock.state + } + } + + // TODO: forward all the other protocol requirements!! +} + + +@Observable +class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { + let logger = Logger(subsystem: "edu.stanford.NAMS", category: "DeviceCoordinator") + + private(set) var connectedDevice: SomeDevice? + + var isConnected: Bool { + connectedDevice != nil + } + + + required init() {} + + + /// Shorthand for easy previewing devices. + init(mock: MockDevice) { + self.connectedDevice = .mock(mock) + } + + + @MainActor + func tapDevice(_ device: SomeDevice) async { + if let connectedDevice { + logger.info("Disconnecting previously connected device \(connectedDevice.label)...") + // either we tapped on the same device or on another one, in any case disconnect the currently connected device + await connectedDevice.disconnect() + self.connectedDevice = nil + + if connectedDevice == device { + // if the tapped one was the connected one return + return + } + } + + logger.info("Connecting to nearby device \(device.label)...") + + await device.connect() + self.connectedDevice = device + } + + func hintDisconnect() { + // TODO: this must also trigger on an external disconnect! + self.connectedDevice = nil // This is not ideal right now + } +} diff --git a/NAMS/Devices/DevicesSheet.swift b/NAMS/Devices/DevicesSheet.swift deleted file mode 100644 index 7df5012..0000000 --- a/NAMS/Devices/DevicesSheet.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Spezi -import SpeziBluetooth -import SwiftUI - - -private enum DeviceType: String, CaseIterable, CustomLocalizedStringResourceConvertible, Hashable { - case muse - case biopot - - var localizedStringResource: LocalizedStringResource { - switch self { - case .muse: - "Muse" - case .biopot: - "Biopot" - } - } -} - - -struct DevicesSheet: View { - private let eegModel: EEGViewModel - - @State private var selectedDevice: DeviceType = .muse - - @Environment(\.dismiss) - private var dismiss - - var body: some View { - NavigationStack { - List { - Section { - Picker("Device Type", selection: $selectedDevice) { - ForEach(DeviceType.allCases, id: \.self) { type in - Text(type.localizedStringResource) - .tag(type) - } - } - .pickerStyle(.segmented) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - } - .listSectionSpacing(.compact) - - switch selectedDevice { - case .muse: - NearbyDevices(eegModel: eegModel) - case .biopot: - Biopot() - } - } - .navigationTitle("NEARBY_DEVICES") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - Button("Close") { - dismiss() - } - } - } - } - - - init(eegModel: EEGViewModel) { - self.eegModel = eegModel - } -} - - -#if DEBUG -#Preview { - DevicesSheet(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) - .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } - } -} - -#Preview { - DevicesSheet(eegModel: EEGViewModel(deviceManager: MockDeviceManager(nearbyDevices: []))) - .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } - } -} -#endif diff --git a/NAMS/Devices/EEGDeviceList.swift b/NAMS/Devices/EEGDeviceList.swift deleted file mode 100644 index 82a5473..0000000 --- a/NAMS/Devices/EEGDeviceList.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct EEGDeviceList: View { - private let eegModel: EEGViewModel - - var sortedDeviceList: [EEGDevice] { - eegModel.nearbyDevices.sorted { lhs, rhs in - lhs.name < rhs.name - } - } - - var body: some View { - ForEach(sortedDeviceList, id: \.macAddress) { device in - EEGDeviceRow(eegModel: eegModel, device: device) - } - } - - - init(eegModel: EEGViewModel) { - self.eegModel = eegModel - } -} - - -#if DEBUG -#Preview { - let model = EEGViewModel(deviceManager: MockDeviceManager(immediate: true)) - return List { - EEGDeviceList(eegModel: model) - } -} -#endif diff --git a/NAMS/Devices/EEGDeviceRow.swift b/NAMS/Devices/EEGDeviceRow.swift deleted file mode 100644 index 4f35c2e..0000000 --- a/NAMS/Devices/EEGDeviceRow.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct EEGDeviceRow: View { - private let device: EEGDevice - private let eegModel: EEGViewModel - - var connectedDevice: ConnectedDevice? { - if let activeDevice = eegModel.activeDevice, - activeDevice.device.macAddress == device.macAddress { - return activeDevice - } - return nil - } - - @State private var presentingActiveDevice: ConnectedDevice? - - var body: some View { - HStack { - deviceButton - - detailsButton - } - .navigationDestination(item: $presentingActiveDevice) { item in - EEGDeviceDetails(device: item) - } - .accessibilityRepresentation { - let button = Button(action: { - deviceButtonAction() - }) { - Text(verbatim: device.model) // currently, this order flows best for Muse device naming - Text(verbatim: device.name) - if device.connectionState.associatedConnection { - Text(device.connectionState.localizedStringResource) - } - } - - // accessibility actions cannot be unit tested - if !FeatureFlags.renderAccessibilityActions { - if let connectedDevice, connectedDevice.state.establishedConnection { - button.accessibilityAction(named: "DEVICE_DETAILS", { - detailsButtonAction(for: connectedDevice) - }) - } else { - button - } - } else { - HStack { - button - .frame(maxWidth: .infinity) - detailsButton - } - } - } - } - - - @MainActor @ViewBuilder private var deviceButton: some View { - Button(action: { - deviceButtonAction() - }) { - HStack { - Text(verbatim: "\(device.model) - \(device.name)") - .foregroundColor(.primary) - Spacer() - - if let connectedDevice { - switch connectedDevice.state { - case .connecting: - ProgressView() - case .connected: - Text("CONNECTED") - .foregroundStyle(.gray) - case .interventionRequired: - Text("ATTENTION_REQUIRED") - .foregroundStyle(.gray) - default: - EmptyView() - } - } - } - .frame(maxWidth: .infinity) - } - } - - @ViewBuilder private var detailsButton: some View { - if let connectedDevice, connectedDevice.state.establishedConnection { - Button("DEVICE_DETAILS", systemImage: "info.circle") { - detailsButtonAction(for: connectedDevice) - } - .labelStyle(.iconOnly) - .font(.title3) - .buttonStyle(.plain) // ensure button is clickable next to the other button - .foregroundColor(.accentColor) - } - } - - - init(eegModel: EEGViewModel, device: EEGDevice) { - self.device = device - self.eegModel = eegModel - } - - - @MainActor - private func deviceButtonAction() { - eegModel.tapDevice(device) - } - - private func detailsButtonAction(for device: ConnectedDevice) { - presentingActiveDevice = device - } -} - - -#if DEBUG -#Preview { - let device = MockEEGDevice(name: "Device", model: "Mock") - let model = EEGViewModel(deviceManager: MockDeviceManager()) - - return NavigationStack { - List { - EEGDeviceRow(eegModel: model, device: device) // tap to pair - } - } -} - -#Preview { - NavigationStack { - List { - EEGDeviceRow( - eegModel: EEGViewModel(deviceManager: MockDeviceManager()), - device: MockEEGDevice(name: "Nearby Device", model: "Mock") - ) - } - } -} - -#Preview { - let devices = [ - MockEEGDevice(name: "Device 1", model: "Mock", state: .connecting), - MockEEGDevice(name: "Device 2", model: "Mock", state: .connected), - MockEEGDevice(name: "Device 3", model: "Mock", state: .interventionRequired("Firmware update required.")) - ] - - return ForEach(devices, id: \.macAddress) { device in - NavigationStack { - List { - EEGDeviceRow(eegModel: EEGViewModel(mock: device), device: device) - } - } - } -} -#endif diff --git a/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift b/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift new file mode 100644 index 0000000..e7630f4 --- /dev/null +++ b/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift @@ -0,0 +1,104 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziBluetooth +import SwiftUI + + +struct BluetoothStateHints: View { + private let state: BluetoothState + + + private var titleMessage: LocalizedStringResource? { + switch state { + case .poweredOn: + nil + case .poweredOff: + "Bluetooth Off" + case .unauthorized: + "Bluetooth Prohibited" + case .unsupported: + "Bluetooth Unsupported" + case .unknown: + "Bluetooth Failure" + } + } + + private var subtitleMessage: LocalizedStringResource? { + switch state { + case .poweredOn: + nil + case .poweredOff: + "BLUETOOTH_OFF_HINT" + case .unauthorized: + "BLUETOOTH_PROHIBITED_HINT" + case .unknown: + "BLUETOOTH_UNKNOWN" + case .unsupported: + "BLUETOOTH_UNSUPPORTED" + } + } + + + var body: some View { + if titleMessage != nil || subtitleMessage != nil { + VStack { + if let titleMessage { + Text(titleMessage) + .bold() + .font(.title2) + .padding(.bottom, 8) + .accessibilityAddTraits(.isHeader) + } + + if let subtitleMessage { + Text(subtitleMessage) + .multilineTextAlignment(.center) + } + } + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: -15, leading: 0, bottom: 0, trailing: 0)) + .padding([.top, .leading, .trailing]) + .frame(maxWidth: .infinity) + } else { + EmptyView() + } + } + + + init(state: BluetoothState) { + self.state = state + } +} + + +#if DEBUG +#Preview { + List { + BluetoothStateHints(state: .poweredOff) + } +} + +#Preview { + List { + BluetoothStateHints(state: .unauthorized) + } +} + +#Preview { + List { + BluetoothStateHints(state: .unsupported) + } +} + +#Preview { + List { + BluetoothStateHints(state: .unknown) + } +} +#endif diff --git a/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift b/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift new file mode 100644 index 0000000..0f3831a --- /dev/null +++ b/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift @@ -0,0 +1,54 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct LoadingSectionHeader: View { + private let text: Text + private let loading: Bool + + var body: some View { + HStack { + text + if loading { + ProgressView() + .padding(.leading, 4) + .accessibilityRemoveTraits(.updatesFrequently) + } + } + } + + @_disfavoredOverload + init(verbatim: String, loading: Bool) { + self.init(Text(verbatim), loading: loading) + } + + init(_ title: LocalizedStringResource, loading: Bool) { + self.init(Text(title), loading: loading) + } + + + init(_ text: Text, loading: Bool) { + self.text = text + self.loading = loading + } +} + + +#if DEBUG +#Preview { + List { + Section { + Text("...") + } header: { + LoadingSectionHeader(verbatim: "Devices", loading: true) + } + } +} +#endif diff --git a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift new file mode 100644 index 0000000..f9b2f59 --- /dev/null +++ b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift @@ -0,0 +1,184 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziBluetooth +import SwiftUI + + +public protocol GenericBluetoothPeripheral { + var label: String { get } + + var accessibilityLabel: String { get } + + var state: PeripheralState { get } + + var requiresUserAttention: Bool { get } // TODO: optional? +} + + +extension GenericBluetoothPeripheral { + public var accessibilityLabel: String { + label + } + + public var requiresUserAttention: Bool { + false + } +} + + +struct MockBluetoothDevice: GenericBluetoothPeripheral { + var label: String + var state: PeripheralState + var requiresUserAttention: Bool + + init(label: String, state: PeripheralState, requiresUserAttention: Bool = false) { + self.label = label + self.state = state + self.requiresUserAttention = requiresUserAttention + } +} + + +public struct NearbyDeviceRow: View { + private let peripheral: any GenericBluetoothPeripheral + private let devicePrimaryActionClosure: () -> Void + private let secondaryActionClosure: (() -> Void)? + + + var localizationSecondaryLabel: LocalizedStringResource? { + if peripheral.requiresUserAttention { + return "Intervention Required" + } + switch peripheral.state { + case .connecting: + return "Connecting" + case .connected: + return "Connected" + case .disconnecting: + return "Disconnecting" + case .disconnected: + return nil + } + } + + public var body: some View { + let stack = HStack { + Button(action: devicePrimaryAction) { + HStack { + // TODO: allow for italics "unknown device"? + ListRow(verbatim: peripheral.label) { + deviceSecondaryLabel + } + if peripheral.state == .connecting || peripheral.state == .disconnecting { + ProgressView() + .accessibilityRemoveTraits(.updatesFrequently) + } + } + } + .frame(maxWidth: .infinity) // required for UI tests // TODO: does this break stuff? + + if secondaryActionClosure != nil, case .connected = peripheral.state { + Button("DEVICE_DETAILS", systemImage: "info.circle", action: deviceDetailsAction) + .labelStyle(.iconOnly) + .font(.title3) + .buttonStyle(.plain) // ensure button is clickable next to the other button + .foregroundColor(.accentColor) + } + } + + #if DEBUG + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { // TODO: verify flag! + // accessibility actions cannot be unit tested + stack + } else { + stack.accessibilityRepresentation { + accessibilityRepresentation + } + } + #else + stack.accessibilityRepresentation { + accessibilityRepresentation + } + #endif + } + + @ViewBuilder var accessibilityRepresentation: some View { + let button = Button(action: devicePrimaryAction) { + // TODO: how to provide a different primary label (for Muse?)? + Text(verbatim: peripheral.accessibilityLabel) + if let localizationSecondaryLabel { + Text(localizationSecondaryLabel) + } + } + + if secondaryActionClosure != nil { + button + .accessibilityAction(named: "DEVICE_DETAILS", deviceDetailsAction) + } else { + button + } + } + + @ViewBuilder var deviceSecondaryLabel: some View { + if peripheral.requiresUserAttention { + Text("Requires Attention") + } else { + switch peripheral.state { + case .connecting, .disconnecting: + EmptyView() + case .connected: + Text("Connected") + case .disconnected: + EmptyView() + } + } + } + + + public init( + peripheral: any GenericBluetoothPeripheral, + primaryAction: @escaping () -> Void, + secondaryAction: (() -> Void)? = nil + ) { + self.peripheral = peripheral + self.devicePrimaryActionClosure = primaryAction + self.secondaryActionClosure = secondaryAction + } + + + private func devicePrimaryAction() { + devicePrimaryActionClosure() + } + + private func deviceDetailsAction() { + if let secondaryActionClosure { + secondaryActionClosure() + } + } +} + + +#if DEBUG +#Preview { + List { + NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 1", state: .connecting)) { + print("Clicked") + } secondaryAction: {} + NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 2", state: .connected)) { + print("Clicked") + } secondaryAction: {} + NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "Long MyDevice 3", state: .connected, requiresUserAttention: true)) { + print("Clicked") + } secondaryAction: {} + NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 4", state: .disconnecting)) { + print("Clicked") + } secondaryAction: {} + } +} +#endif diff --git a/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift b/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift deleted file mode 100644 index 3ea9913..0000000 --- a/NAMS/Devices/Mock/EEGViewModel+ActiveMock.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -extension EEGViewModel { - convenience init(mock: MockEEGDevice) { - self.init(deviceManager: MockDeviceManager()) - let activeDevice = ConnectedDevice(device: mock, session: recordingSessionBinding) - sinkActiveDevice(device: activeDevice) - activeDevice.connect() - self.activeDevice = activeDevice - } -} diff --git a/NAMS/Devices/Mock/MockDevice.swift b/NAMS/Devices/Mock/MockDevice.swift new file mode 100644 index 0000000..c543606 --- /dev/null +++ b/NAMS/Devices/Mock/MockDevice.swift @@ -0,0 +1,165 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziBluetooth + + +@Observable +class MockDevice { + private static let sampleRate = 60 + + let id: UUID + let name: String + private let eegMeasurementGenerators: [EEGFrequency: MockMeasurementGenerator] + + var state: PeripheralState + var deviceInformation: MuseDeviceInformation? // we are just reusing muse data model + + /// The currently associated recording session. + @MainActor private var recordingSession: EEGRecordingSession? + + var connectionState: ConnectionState { + switch state { + case .disconnected: + return .disconnected + case .connecting: + return .connecting + case .connected: + return .connected + case .disconnecting: + return .disconnected + } + } + + @ObservationIgnored private var eegTimer: Timer? { + willSet { + eegTimer?.invalidate() + } + } + @ObservationIgnored private var task: Task? { + willSet { + task?.cancel() + } + } + + + init(name: String, state: PeripheralState = .disconnected) { + self.id = UUID() + self.name = name + self.state = state + self.eegMeasurementGenerators = EEGFrequency.allCases.reduce(into: [:]) { result, frequency in + result[frequency] = MockMeasurementGenerator(sampleRate: Self.sampleRate) + } + + switch state { + case .connecting: + connect() + case .connected: + handleConnected() + case .disconnecting: + disconnect() + case .disconnected: + break + } + } + + + func connect() { + state = .connecting + task = Task { @MainActor [weak self] in + try? await Task.sleep(for: .seconds(2.5)) + guard !Task.isCancelled, + let self = self, + self.state == .connecting else { + return + } + self.state = .connected + self.handleConnected() + } + } + + private func handleConnected() { + self.deviceInformation = MuseDeviceInformation(serialNumber: "0xAABBCCDD", firmwareVersion: "1.2.0", remainingBatteryPercentage: 75) + + task = Task { @MainActor [weak self] in + try? await Task.sleep(for: .seconds(3)) + guard !Task.isCancelled, + let self = self, + self.state == .connected, + let info = self.deviceInformation else { + return + } + info.fit = HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .good) + info.wearingHeadband = true + } + } + + func disconnect() { + state = .disconnected + deviceInformation = nil + task = nil + eegTimer = nil + } + + @MainActor + func startRecording(_ session: EEGRecordingSession) async throws { + self.recordingSession = session + + // schedule timer to generate fake EEG data + let timer = Timer(timeInterval: 1.0 / Double(Self.sampleRate), repeats: true, block: generateRecording) + RunLoop.main.add(timer, forMode: .common) + self.eegTimer = timer + + generateRecording(timer: timer) // make sure there is data instantly + } + + @MainActor + func stopRecording() { + self.eegTimer = nil + self.recordingSession = nil + } + + @Sendable + private func generateRecording(timer: Timer) { + // its running on the main RunLoop so this is safe to assume + MainActor.assumeIsolated { + guard let recordingSession, + state == .connected else { + timer.invalidate() + return + } + + for (frequency, generator) in eegMeasurementGenerators { + let series = generator.next() + recordingSession.append(series: series, for: frequency) + } + } + } +} + + +extension MockDevice: GenericBluetoothPeripheral { + var label: String { + name + } +} + + +extension MockDevice: Identifiable {} + + +extension MockDevice: Hashable { + static func == (lhs: MockDevice, rhs: MockDevice) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/NAMS/Devices/Mock/MockDeviceManager.swift b/NAMS/Devices/Mock/MockDeviceManager.swift index 06de8bd..dfcb07f 100644 --- a/NAMS/Devices/Mock/MockDeviceManager.swift +++ b/NAMS/Devices/Mock/MockDeviceManager.swift @@ -6,44 +6,68 @@ // SPDX-License-Identifier: MIT // -import Combine +import Observation +import SpeziBluetooth -class MockDeviceManager: DeviceManager { - static var defaultNearbyDevices: [EEGDevice] { +@Observable +class MockDeviceManager { + static var defaultNearbyDevices: [MockDevice] { [ - MockEEGDevice(name: "Device 2", model: "Mock"), - MockEEGDevice(name: "Device 1", model: "Mock") + MockDevice(name: "Mock Device 1"), + MockDevice(name: "Mock Device 2") ] } - @Published var nearbyDevices: [EEGDevice] = [] + private let storedDevicesList: [MockDevice] - let deviceList: [EEGDevice] - - var devicePublisher: Published<[EEGDevice]>.Publisher { - $nearbyDevices + // TODO: isScanning property? + var nearbyDevices: [MockDevice] = [] + @ObservationIgnored private var task: Task? { + willSet { + task?.cancel() + } } - init(nearbyDevices: [EEGDevice] = MockDeviceManager.defaultNearbyDevices, immediate: Bool = false) { - self.deviceList = nearbyDevices + init(nearbyDevices: [MockDevice] = MockDeviceManager.defaultNearbyDevices, immediate: Bool = false) { + self.storedDevicesList = nearbyDevices if immediate { - self.nearbyDevices = deviceList + self.nearbyDevices = nearbyDevices } } func startScanning() { - Task { @MainActor in - try? await Task.sleep(for: .seconds(1)) - nearbyDevices = deviceList + task = Task { @MainActor in + // TODO: instant discovery of previously discovered devices + try? await Task.sleep(for: .seconds(2)) + guard !Task.isCancelled else { + return + } + nearbyDevices = storedDevicesList } } - func stopScanning() {} + func stopScanning() { + task = Task { @MainActor in + nearbyDevices.removeAll { device in + device.state == .disconnected + } + } + } +} + + +extension MockDeviceManager: BluetoothScanner { + var hasConnectedDevices: Bool { + nearbyDevices.contains { device in + device.state != .disconnected + } + } - func retrieveDeviceList() -> [EEGDevice] { - nearbyDevices + func scanNearbyDevices(autoConnect: Bool) async { + precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") + self.startScanning() } } diff --git a/NAMS/Devices/Mock/MockEEGDevice.swift b/NAMS/Devices/Mock/MockEEGDevice.swift deleted file mode 100644 index 40be3ef..0000000 --- a/NAMS/Devices/Mock/MockEEGDevice.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -private class ConnectionListener: DeviceConnectionListener { - private static let sampleRate = 60 - - private let mockDevice: MockEEGDevice - private let device: ConnectedDevice - - private let eegMeasurementGenerators: [EEGFrequency: EEGMeasurementGenerator] - - init(mock mockDevice: MockEEGDevice, device: ConnectedDevice) { - self.mockDevice = mockDevice - self.device = device - self.eegMeasurementGenerators = EEGFrequency.allCases.reduce(into: [:]) { result, frequency in - result[frequency] = EEGMeasurementGenerator(sampleRate: Self.sampleRate) - } - } - - - func connect() { - if mockDevice.connectionState.associatedConnection { - device.state = mockDevice.connectionState - if device.state.establishedConnection { - onConnected() - } - return - } - - change(connectionState: .connecting) - Task { @MainActor in - try? await Task.sleep(for: .seconds(2)) - change(connectionState: .connected) - onConnected() - } - } - - func onConnected() { - device.remainingBatteryPercentage = 75 - device.aboutInformation = [ - "SERIAL_NUMBER": "AA BB CC DD", - "FIRMWARE_VERSION": "1.2.0" - ] - - // timer cancels itself based on the connection state - let timer = Timer(timeInterval: 1.0 / Double(Self.sampleRate), repeats: true, block: generateRecording) - RunLoop.main.add(timer, forMode: .common) - - Task { @MainActor in - try await Task.sleep(for: .seconds(3)) - device.fit = HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .good) - device.wearingHeadband = true - } - } - - func change(connectionState: ConnectionState) { - mockDevice.connectionState = connectionState - device.state = connectionState - } - - @Sendable - private func generateRecording(timer: Timer) { - if mockDevice.connectionState != .connected { - timer.invalidate() - return - } - - for (frequency, generator) in eegMeasurementGenerators { - device.session?.measurements[frequency, default: []] - .append(generator.next()) - } - } -} - - -class MockEEGDevice: EEGDevice { - let name: String - let model: String - let macAddress: String - - var connectionState: ConnectionState - var rssi: Double = 0 - var lastDiscoveredTime: Double = 0 - - private var lastListener: ConnectionListener? - - init(name: String, model: String, macAddress: String? = nil, state: ConnectionState = .unknown) { - self.name = name - self.model = model - self.macAddress = macAddress ?? (0..<6) - .map { _ in String(format: "%02X", Int.random(in: 0...255)) } - .joined(separator: ":") - self.connectionState = state - } - - - func connect(state device: ConnectedDevice) -> DeviceConnectionListener { - let listener = ConnectionListener(mock: self, device: device) - listener.connect() - self.lastListener = listener - return listener - } - - func disconnect() { - lastListener?.change(connectionState: .disconnected) - connectionState = .disconnected - lastListener = nil - } -} diff --git a/NAMS/Devices/Mock/EEGMeasurementGenerator.swift b/NAMS/Devices/Mock/MockMeasurementGenerator.swift similarity index 98% rename from NAMS/Devices/Mock/EEGMeasurementGenerator.swift rename to NAMS/Devices/Mock/MockMeasurementGenerator.swift index 0557fc9..e06e2dc 100644 --- a/NAMS/Devices/Mock/EEGMeasurementGenerator.swift +++ b/NAMS/Devices/Mock/MockMeasurementGenerator.swift @@ -9,7 +9,7 @@ import Foundation -class EEGMeasurementGenerator { +class MockMeasurementGenerator { private let sampleRate: Int private let baseValue: Double diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift new file mode 100644 index 0000000..549d3a4 --- /dev/null +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -0,0 +1,51 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MockDeviceRow: View { + private let device: MockDevice + + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + + @State private var presentingActiveDevice: MockDevice? + + + var body: some View { + NearbyDeviceRow(peripheral: device) { + Task { + await deviceCoordinator.tapDevice(.mock(device)) + } + } secondaryAction: { + if device.state == .connected { + presentingActiveDevice = device + } + } + .navigationDestination(item: $presentingActiveDevice) { device in + if let info = device.deviceInformation { + MuseDeviceDetailsView(model: device.label, state: device.connectionState, info) { + device.disconnect() + // TODO: this needs a better approach + deviceCoordinator.hintDisconnect() + } + } + } + } + + + init(device: MockDevice) { + self.device = device + } +} + + +#if DEBUG +// TODO: preview +#endif diff --git a/NAMS/Devices/Model/ConnectedDevice.swift b/NAMS/Devices/Model/ConnectedDevice.swift deleted file mode 100644 index f619a80..0000000 --- a/NAMS/Devices/Model/ConnectedDevice.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import OrderedCollections -import SwiftUI - - -@Observable -class ConnectedDevice { - let device: EEGDevice - var listener: DeviceConnectionListener? - - var state: ConnectionState { - get { - access(keyPath: \.state) - return publishedState - } - set { - withMutation(keyPath: \.state) { - publishedState = newValue - } - } - } - - @ObservationIgnored @Published var publishedState: ConnectionState = .unknown // current workaround to create a publisher for the view model - - // artifacts muse supports - var wearingHeadband = false - var eyeBlink = false - var jawClench = false - - /// Determines if the last second of data is considered good - var isGood: (Bool, Bool, Bool, Bool) = (false, false, false, false) // swiftlint:disable:this large_tuple - /// The current fit of the headband - var fit = HeadbandFit(tp9Fit: .poor, af7Fit: .poor, af8Fit: .poor, tp10Fit: .poor) - - var aboutInformation: OrderedDictionary = [:] - - /// Remaining battery percentage in percent [0.0;100.0] - var remainingBatteryPercentage: Double? - - @Binding @ObservationIgnored var session: EEGRecordingSession? - - init(device: EEGDevice, session: Binding) { - self.device = device - self._session = session - } - - func connect() { - listener = device.connect(state: self) - } - - func disconnect() { - device.disconnect() - listener = nil - } -} - - -extension ConnectedDevice: Hashable { - static func == (lhs: ConnectedDevice, rhs: ConnectedDevice) -> Bool { - lhs.device.macAddress == rhs.device.macAddress - } - - func hash(into hasher: inout Hasher) { - device.macAddress.hash(into: &hasher) - } -} - - -extension LocalizedStringResource: Hashable { - public static func == (lhs: LocalizedStringResource, rhs: LocalizedStringResource) -> Bool { - lhs.key == rhs.key - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(key) - } -} diff --git a/NAMS/Devices/Model/DeviceConnectionListener.swift b/NAMS/Devices/Model/DeviceConnectionListener.swift deleted file mode 100644 index 46a2cff..0000000 --- a/NAMS/Devices/Model/DeviceConnectionListener.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -protocol DeviceConnectionListener {} diff --git a/NAMS/Devices/Model/DeviceManager.swift b/NAMS/Devices/Model/DeviceManager.swift deleted file mode 100644 index 1939a83..0000000 --- a/NAMS/Devices/Model/DeviceManager.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Combine - - -protocol DeviceManager: AnyObject { - var devicePublisher: Published<[EEGDevice]>.Publisher { get } - - func startScanning() - - func stopScanning() - - func retrieveDeviceList() -> [EEGDevice] -} diff --git a/NAMS/Devices/Model/EEGDevice.swift b/NAMS/Devices/Model/EEGDevice.swift deleted file mode 100644 index d519225..0000000 --- a/NAMS/Devices/Model/EEGDevice.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -protocol EEGDevice: AnyObject { - var name: String { get } - var macAddress: String { get } - var model: String { get } - - var connectionState: ConnectionState { get } - var rssi: Double { get } - - var lastDiscoveredTime: Double { get } - - func connect(state device: ConnectedDevice) -> DeviceConnectionListener - - func disconnect() -} diff --git a/NAMS/Devices/Model/EEGViewModel.swift b/NAMS/Devices/Model/EEGViewModel.swift deleted file mode 100644 index 593f18f..0000000 --- a/NAMS/Devices/Model/EEGViewModel.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Combine -import OSLog -import SwiftUI - - -@Observable -class EEGViewModel { - let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseViewModel") - - private let deviceManager: DeviceManager - - var nearbyDevices: [EEGDevice] = [] - var activeDevice: ConnectedDevice? - private(set) var recordingSession: EEGRecordingSession? - - private var deviceManagerCancelable: AnyCancellable? - private var activeDeviceCancelable: AnyCancellable? - - var recordingSessionBinding: Binding { - Binding { - self.recordingSession - } set: { newValue in - self.recordingSession = newValue - } - } - - init(deviceManager: DeviceManager) { - self.deviceManager = deviceManager - self.deviceManagerCancelable = nil - - self.deviceManagerCancelable = self.deviceManager.devicePublisher.sink { [weak self] devices in - self?.nearbyDevices = devices - self?.logger.debug("Updated nearby devices to \(devices.count) in total.") - } - } - - @MainActor - func startRecordingSession() { - self.recordingSession = EEGRecordingSession() - } - - @MainActor - func stopRecordingSession() { - self.recordingSession = nil - } - - - @MainActor - private func refreshNearbyDevices() { - self.nearbyDevices = self.deviceManager.retrieveDeviceList() - } - - @MainActor - func startScanning() { - logger.debug("Start scanning for nearby devices...") - self.deviceManager.startScanning() - refreshNearbyDevices() - - if !nearbyDevices.isEmpty { - logger.debug("Found \(self.nearbyDevices.count) nearby devices immediately.") - } - } - - @MainActor - func stopScanning(refreshNearby: Bool = true) { - logger.debug("Stopped scanning for nearby devices!") - self.deviceManager.stopScanning() - - if refreshNearby { - refreshNearbyDevices() - logger.debug("We maintain \(self.nearbyDevices.count) devices after scanning stop.") - } - } - - @MainActor - func tapDevice(_ device: EEGDevice) { - if let activeDevice { - logger.info("Disconnecting previously connected device \(activeDevice.device.name)...") - // either we tapped on the same Muse or on another one, in any case disconnect the currently active Muse - activeDevice.disconnect() - clearActiveDevice() - - - if activeDevice.device.macAddress == device.macAddress { - // if the tapped one was the active one return - return - } - } - - logger.info("Connecting to nearby devices \(device.name)...") - - let activeDevice = ConnectedDevice(device: device, session: recordingSessionBinding) - sinkActiveDevice(device: activeDevice) - - activeDevice.connect() - self.activeDevice = activeDevice - } - - func sinkActiveDevice(device: ConnectedDevice) { - activeDeviceCancelable = device.$publishedState.sink { [weak self] state in - if case .disconnected = state { - self?.clearActiveDevice() - } - } - } - - private func clearActiveDevice() { - activeDeviceCancelable?.cancel() - activeDeviceCancelable = nil - activeDevice = nil - } -} diff --git a/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift b/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift deleted file mode 100644 index f55d5cb..0000000 --- a/NAMS/Devices/Muse/IXNMuse+EEGDevice.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -#if MUSE -extension IXNMuse: EEGDevice { - var name: String { - getName().replacingOccurrences(of: "Muse-", with: "") - } - - var macAddress: String { - getMacAddress() - } - - var model: String { - getModel().description - } - - var connectionState: ConnectionState { - ConnectionState(from: getConnectionState()) - } - - var rssi: Double { - getRssi() - } - - var lastDiscoveredTime: Double { - getLastDiscoveredTime() - } - - - func connect(state device: ConnectedDevice) -> DeviceConnectionListener { - let listener = MuseConnectionListener(muse: self, device: device) - listener.connect() - return listener - } -} -#endif diff --git a/NAMS/Devices/Model/ConnectionState.swift b/NAMS/Devices/Muse/Model/ConnectionState.swift similarity index 94% rename from NAMS/Devices/Model/ConnectionState.swift rename to NAMS/Devices/Muse/Model/ConnectionState.swift index 32556b7..d0a0c17 100644 --- a/NAMS/Devices/Model/ConnectionState.swift +++ b/NAMS/Devices/Muse/Model/ConnectionState.swift @@ -9,6 +9,8 @@ import Foundation +// TODO: move some of these files to the Muse folder? + enum ConnectionState { case unknown case connected @@ -40,6 +42,7 @@ enum ConnectionState { extension ConnectionState: Equatable {} +// TODO: are any of these still used? extension ConnectionState: CustomLocalizedStringResourceConvertible { public var localizedStringResource: LocalizedStringResource { switch self { diff --git a/NAMS/Devices/Model/Fit.swift b/NAMS/Devices/Muse/Model/Fit.swift similarity index 100% rename from NAMS/Devices/Model/Fit.swift rename to NAMS/Devices/Muse/Model/Fit.swift diff --git a/NAMS/Devices/Model/HeadbandFit.swift b/NAMS/Devices/Muse/Model/HeadbandFit.swift similarity index 100% rename from NAMS/Devices/Model/HeadbandFit.swift rename to NAMS/Devices/Muse/Model/HeadbandFit.swift diff --git a/NAMS/Devices/Muse/MuseConnectionListener.swift b/NAMS/Devices/Muse/MuseConnectionListener.swift deleted file mode 100644 index 0baac30..0000000 --- a/NAMS/Devices/Muse/MuseConnectionListener.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import OSLog - - -#if MUSE -class MuseConnectionListener: DeviceConnectionListener, IXNMuseConnectionListener, IXNMuseDataListener { - private let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseConnectionListener") - - private let muse: IXNMuse - private let device: ConnectedDevice - - init(muse: IXNMuse, device: ConnectedDevice) { - self.muse = muse - self.device = device - } - - func connect() { - muse.register(self) - - muse.register(self, type: .artifacts) - muse.register(self, type: .battery) - muse.register(self, type: .isGood) - - // Might want to read https://www.learningeeg.com/terminology-and-waveforms for a short intro into EEG frequency ranges - muse.register(self, type: .thetaAbsolute) // 4-8 Hz - muse.register(self, type: .alphaAbsolute) // 8-16 Hz - muse.register(self, type: .betaAbsolute) // 16-32 Hz - muse.register(self, type: .gammaAbsolute) // 32-64 Hz - - muse.register(self, type: .hsiPrecision) // we don't yet visualize this - - // set the preset manually for now - switch muse.getModel() { - case .mu01, .mu02: - break - case .mu03, .mu04, .mu05: - muse.setPreset(.preset53) - @unknown default: - break - } - - muse.runAsynchronously() - } - - func receive(_ packet: IXNMuseConnectionPacket, muse: IXNMuse?) { - device.state = ConnectionState(from: packet.currentConnectionState) - logger.debug("\(self.muse.getName()) state is now \(self.device.state.description)") - - switch device.state { - case .connected: - logger.debug("\(self.muse.getModel()) - \(self.muse.getName()): Connected. Versions: \(self.muse.getVersion()?.versionString ?? "NONE"); Configuration: \(self.muse.getConfiguration())") - - if let version = self.muse.getVersion() { - logger.debug("\(self.muse.getModel()) - \(self.muse.getName()): Versions: \(version.versionString)") - - device.aboutInformation["FIRMWARE_VERSION"] = version.getFirmwareVersion() - } - - if let configuration = self.muse.getConfiguration() { - device.remainingBatteryPercentage = configuration.getBatteryPercentRemaining() - - logger.debug("\(self.muse.getModel()) - \(self.muse.getName()): Configuration: \(configuration.configurationString)") - - device.aboutInformation["SERIAL_NUMBER"] = configuration.getSerialNumber() - } - case .disconnected: - device.remainingBatteryPercentage = nil - device.wearingHeadband = false - device.eyeBlink = false - device.jawClench = false - device.isGood = (false, false, false, false) - - device.session = nil - - self.muse.unregisterAllListeners() - device.listener = nil - default: - break - } - } - - func receive(_ packet: IXNMuseDataPacket?, muse: IXNMuse?) { // swiftlint:disable:this cyclomatic_complexity - guard let packet else { - return - } - - switch packet.packetType() { - case .hsiPrecision: - let fit = HeadbandFit(from: packet) - if device.fit != fit { - device.fit = fit - } - case .eeg: - device.session?.measurements[.all, default: []].append(EEGSeries(from: packet)) - case .thetaAbsolute: - device.session?.measurements[.theta, default: []].append(EEGSeries(from: packet)) - case .alphaAbsolute: - device.session?.measurements[.alpha, default: []].append(EEGSeries(from: packet)) - case .betaAbsolute: - device.session?.measurements[.beta, default: []].append(EEGSeries(from: packet)) - case .gammaAbsolute: - device.session?.measurements[.gamma, default: []].append(EEGSeries(from: packet)) - case .battery: - device.remainingBatteryPercentage = packet.getBatteryValue(.chargePercentageRemaining) - logger.debug("Remaining battery percentage: \(packet.getBatteryValue(.chargePercentageRemaining))") - case .isGood: - device.isGood = ( - packet.getEegChannelValue(.EEG1) == 1.0, - packet.getEegChannelValue(.EEG2) == 1.0, - packet.getEegChannelValue(.EEG3) == 1.0, - packet.getEegChannelValue(.EEG4) == 1.0 - ) - default: - break - } - } - - func receive(_ packet: IXNMuseArtifactPacket, muse: IXNMuse?) { - if packet.headbandOn != device.wearingHeadband { - logger.debug("Wearing headband: \(packet.headbandOn)") - device.wearingHeadband = packet.headbandOn - } - - if packet.blink != device.eyeBlink { - device.eyeBlink = packet.blink - if packet.blink { - logger.debug("Detected eye blink") - } - } - - if packet.jawClench != device.jawClench { - device.jawClench = packet.jawClench - if packet.jawClench { - logger.debug("Detected jaw clench") - } - } - } -} -#endif diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift new file mode 100644 index 0000000..cea50a9 --- /dev/null +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -0,0 +1,341 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OSLog +import SpeziBluetooth + + +@Observable +class MuseDeviceInformation { + let serialNumber: String + let firmwareVersion: String + + /// Remaining battery percentage in percent [0.0;100.0] + var remainingBatteryPercentage: Double? + + // artifacts muse supports + var wearingHeadband = false + var eyeBlink = false + var jawClench = false + + /// Determines if the last second of data is considered good + var isGood: (Bool, Bool, Bool, Bool) = (false, false, false, false) // swiftlint:disable:this large_tuple + // TODO: change above thingy! (similar to below!) + + /// The current fit of the headband + var fit: HeadbandFit? + + init( + serialNumber: String, + firmwareVersion: String, + remainingBatteryPercentage: Double?, + wearingHeadband: Bool = false, + fit: HeadbandFit? = nil + ) { + self.serialNumber = serialNumber + self.firmwareVersion = firmwareVersion + self.remainingBatteryPercentage = remainingBatteryPercentage + self.wearingHeadband = wearingHeadband + self.fit = fit + } +} + +#if MUSE +@Observable +class MuseDevice: Identifiable { + /// List of data packets we are registering to. + private static let packetTypes: [IXNMuseDataPacketType] = [ + .artifacts, + .battery, + .isGood, + + // Might want to read https://www.learningeeg.com/terminology-and-waveforms for a short intro into EEG frequency ranges + .thetaAbsolute, // 4-8 Hz + .alphaAbsolute, // 8-16 Hz + .betaAbsolute, // 16-32 Hz + .gammaAbsolute, // 32-64 Hz + // .eeg, // TODO: we are interested in querying ALL data! + + .hsiPrecision + ] + + let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseDevice") + + private let muse: IXNMuse + private var connectionListener: ConnectionListener? + private var dataListener: DataListener? + + var connectionState: ConnectionState + /// The device information, preset if device is connected + var deviceInformation: MuseDeviceInformation? + + /// The currently associated recording session. + @MainActor private var recordingSession: EEGRecordingSession? + + var name: String { + muse.getName().replacingOccurrences(of: "Muse-", with: "") + } + + var id: String { + muse.getMacAddress() + } + + var model: String { + muse.getModel().description + } + + var label: String { + "\(model) - \(name)" + } + + var rssi: Double { + muse.getRssi() + } + + var lastDiscoveredTime: Double { + muse.getLastDiscoveredTime() + } + + var underlyingDevice: IXNMuse { + muse + } + + init(_ muse: IXNMuse) { + self.muse = muse + self.connectionState = ConnectionState(from: muse.getConnectionState()) + + self.connectionListener = ConnectionListener(device: self) + self.muse.setNumConnectTries(0) // TODO: does this interfere with anything? + // TODO: error listener? + } + + func connect() { + dataListener = DataListener(device: self) + + // set the preset manually for now + switch muse.getModel() { + case .mu01, .mu02: + break + case .mu03, .mu04, .mu05: + muse.setPreset(.preset53) + @unknown default: + break + } + + muse.runAsynchronously() + } + + func disconnect() { + muse.disconnect() + } + + @MainActor + func startRecording(_ session: EEGRecordingSession) async throws { + self.recordingSession = session + // TODO: only enable recording upon request? + } + + @MainActor + func stopRecording() { + self.recordingSession = nil + } + + + @MainActor + private func receive(_ packet: IXNMuseConnectionPacket, muse: IXNMuse) { + connectionState = ConnectionState(from: packet.currentConnectionState) + logger.debug("\(self.name) state is now \(self.connectionState.description)") + + switch connectionState { + case .connected: + handleDeviceConnected() + case .disconnected: + deviceInformation = nil + connectionListener = nil + default: + break + } + } + + private func handleDeviceConnected() { + guard let version = muse.getVersion(), + let configuration = muse.getConfiguration() else { + logger.warning("\(self.label): Failed to retrieve device information even though device was reported as connected!") + return + } + + logger.debug("\(self.label): Connected. Versions: \(version.versionString); Configuration: \(configuration.configurationString)") + + self.deviceInformation = MuseDeviceInformation( // TODO: other info that is relevant? + serialNumber: configuration.getSerialNumber(), + firmwareVersion: version.getFirmwareVersion(), + remainingBatteryPercentage: configuration.getBatteryPercentRemaining() + ) + } + + + @MainActor + private func receive(_ packet: IXNMuseDataPacket, muse: IXNMuse) { + switch packet.packetType() { + case .hsiPrecision: + let fit = HeadbandFit(from: packet) + if deviceInformation?.fit != fit { // TODO: replace all the optional accesses (we have a class now) + deviceInformation?.fit = fit + } + case .eeg: + recordingSession?.append(series: EEGSeries(from: packet), for: .all) + case .thetaAbsolute: + recordingSession?.append(series: EEGSeries(from: packet), for: .theta) + case .alphaAbsolute: + recordingSession?.append(series: EEGSeries(from: packet), for: .alpha) + case .betaAbsolute: + recordingSession?.append(series: EEGSeries(from: packet), for: .beta) + case .gammaAbsolute: + recordingSession?.append(series: EEGSeries(from: packet), for: .gamma) + case .battery: + deviceInformation?.remainingBatteryPercentage = packet.getBatteryValue(.chargePercentageRemaining) + case .isGood: + deviceInformation?.isGood = ( + packet.getEegChannelValue(.EEG1) == 1.0, + packet.getEegChannelValue(.EEG2) == 1.0, + packet.getEegChannelValue(.EEG3) == 1.0, + packet.getEegChannelValue(.EEG4) == 1.0 + ) + default: + break + } + } + + @MainActor + private func receive(_ packet: IXNMuseArtifactPacket, muse: IXNMuse) { + if packet.headbandOn != deviceInformation?.wearingHeadband { + logger.debug("Wearing headband: \(packet.headbandOn)") + deviceInformation?.wearingHeadband = packet.headbandOn + } + + if packet.blink != deviceInformation?.eyeBlink { + deviceInformation?.eyeBlink = packet.blink + if packet.blink { + logger.debug("Detected eye blink") + } + } + + if packet.jawClench != deviceInformation?.jawClench { + deviceInformation?.jawClench = packet.jawClench + if packet.jawClench { + logger.debug("Detected jaw clench") + } + } + } + + deinit { + if dataListener != nil { + disconnect() + self.dataListener = nil + } + connectionListener = nil + } +} + +extension MuseDevice: Hashable { + public static func == (lhs: MuseDevice, rhs: MuseDevice) -> Bool { + lhs.muse == rhs.muse + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(muse) + } +} + +extension MuseDevice: GenericBluetoothPeripheral { + var state: PeripheralState { + switch connectionState { + case .disconnected, .unknown: + return .disconnected + case .connecting: + return .connecting + case .connected: + return .connected + case .interventionRequired: + return .connected + } + } +} + + +extension MuseDevice { + private class ConnectionListener: IXNMuseConnectionListener { + private weak var device: MuseDevice? + + init(device: MuseDevice) { + self.device = device + device.muse.register(self) + } + + func receive(_ packet: IXNMuseConnectionPacket, muse: IXNMuse?) { + guard let device, let muse else { + return + } + Task { @MainActor in + device.receive(packet, muse: muse) + } + } + + + deinit { + guard let device else { + preconditionFailure("MuseDevice was deinitialized before \(Self.self) was deinitialized.") + } + device.muse.unregisterConnectionListener(self) + } + } +} + + +extension MuseDevice { + private class DataListener: IXNMuseDataListener { + private weak var device: MuseDevice? + + init(device: MuseDevice) { + self.device = device + + for type in MuseDevice.packetTypes { + device.muse.register(self, type: type) + } + } + + func receive(_ packet: IXNMuseDataPacket?, muse: IXNMuse?) { + guard let device, let packet, let muse else { + return + } + Task { @MainActor in + device.receive(packet, muse: muse) + } + } + + func receive(_ packet: IXNMuseArtifactPacket, muse: IXNMuse?) { + guard let device, let muse else { + return + } + Task { @MainActor in + device.receive(packet, muse: muse) + } + } + + deinit { + guard let device else { + preconditionFailure("MuseDevice was deinitialized before \(Self.self) was deinitialized.") + } + for type in MuseDevice.packetTypes { + device.muse.register(self, type: type) + } + } + } +} +#endif diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 281b7e4..48e546f 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -6,64 +6,106 @@ // SPDX-License-Identifier: MIT // +import Observation import OSLog +import SpeziBluetooth #if MUSE -class MuseDeviceManager: DeviceManager, IXNLogListener { - private class MuseListener: IXNMuseListener { // avoids cyclic references caused by setMuseListener - private unowned let deviceManager: MuseDeviceManager - - - init(deviceManager: MuseDeviceManager) { - self.deviceManager = deviceManager - } - - - func museListChanged() { - deviceManager.nearbyMuses = self.deviceManager.museManager.getMuses() - } - } - +@Observable +class MuseDeviceManager { private let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseDeviceManager") private let museManager: IXNMuseManager - private var museListener: MuseListener? + @ObservationIgnored private var museListener: MuseListener? - @Published var nearbyMuses: [EEGDevice] = [] - - var devicePublisher: Published<[EEGDevice]>.Publisher { - $nearbyMuses - } + /// The list of nearby muse devices. + private(set) var nearbyMuses: [MuseDevice] = [] + // TODO: isScanning property init() { self.museManager = IXNMuseManagerIos() - - self.museListener = nil self.museListener = MuseListener(deviceManager: self) - self.museManager.setMuseListener(museListener) - if let apiVersion = IXNLibmuseVersion.instance() { logger.debug("Initialized Muse Manager with API version \(apiVersion.getString())") } + + self.museManager.removeFromList(after: 6) // stale timeout if there isn't an updated advertisement TODO: verify 5s? } func startScanning() { + logger.debug("Start scanning for nearby Muse devices...") self.museManager.startListening() } func stopScanning() { + logger.debug("Stopped scanning for nearby Muse devices!") + // TODO: check if we are still scanning? self.museManager.stopListening() } - func retrieveDeviceList() -> [EEGDevice] { - self.museManager.getMuses() + + private func handleUpdatedDeviceList() { + let nearbyMuses = museManager.getMuses() + + // remove all muses that went away + for (index, removedMuse) in self.nearbyMuses.enumerated() { + guard !nearbyMuses.contains(removedMuse.underlyingDevice) else { + continue + } + self.nearbyMuses.remove(at: index) + } + + for addedMuse in nearbyMuses { + guard !self.nearbyMuses.contains(where: { $0.underlyingDevice == addedMuse }) else { + continue + } + self.nearbyMuses.append(MuseDevice(addedMuse)) + } + } + + deinit { + self.museListener = nil + } +} + + +extension MuseDeviceManager: BluetoothScanner { + var hasConnectedDevices: Bool { + nearbyMuses.contains { device in + device.state != .disconnected + } + } + + func scanNearbyDevices(autoConnect: Bool) async { + precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") + self.startScanning() } +} + - func receiveLog(_ log: IXNLogPacket) { - // we currently don't register the log manager - logger.debug("\(log.tag): \(log.timestamp) raw:\(log.raw) \(log.message)") +extension MuseDeviceManager { + private class MuseListener: IXNMuseListener { // avoids cyclic references caused by setMuseListener + private weak var deviceManager: MuseDeviceManager? + + + init(deviceManager: MuseDeviceManager) { + self.deviceManager = deviceManager + deviceManager.museManager.setMuseListener(self) + } + + + func museListChanged() { + guard let deviceManager else { + return + } + deviceManager.handleUpdatedDeviceList() + } + + deinit { + deviceManager?.museManager.setMuseListener(nil) + } } } #endif diff --git a/NAMS/Devices/EEGDeviceDetails.swift b/NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift similarity index 52% rename from NAMS/Devices/EEGDeviceDetails.swift rename to NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift index 15a1540..f7a14e5 100644 --- a/NAMS/Devices/EEGDeviceDetails.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift @@ -9,17 +9,20 @@ import SwiftUI -struct EEGDeviceDetails: View { +struct MuseDeviceDetailsView: View { + private let model: String + private let state: ConnectionState + private let deviceInformation: MuseDeviceInformation + private let disconnectClosure: () -> Void + @Environment(\.dismiss) private var dismiss @Environment(\.locale) private var locale - private let device: ConnectedDevice - var body: some View { List { - if case let .interventionRequired(message) = device.state { + if case let .interventionRequired(message) = state { interventionRequiredHeader(message: message) } @@ -28,36 +31,36 @@ struct EEGDeviceDetails: View { headbandFit - if !device.aboutInformation.isEmpty { - Section("About") { - ForEach(device.aboutInformation.elements, id: \.key) { element in - ListRow(element.key) { - Text(verbatim: element.value.description) - } - } + Section("About") { + ListRow("FIRMWARE_VERSION") { + Text(verbatim: deviceInformation.firmwareVersion) + } + ListRow("SERIAL_NUMBER") { + Text(verbatim: deviceInformation.serialNumber) } } Button(action: { - device.disconnect() + disconnectClosure() dismiss() }) { Text("DISCONNECT") .frame(maxWidth: .infinity) } - .disabled(!device.state.associatedConnection) + .disabled(!state.associatedConnection) } - .navigationTitle(Text(verbatim: device.device.model)) + .navigationTitle(Text(verbatim: model)) .navigationBarTitleDisplayMode(.inline) } @ViewBuilder private var battery: some View { - if let remainingBattery = device.remainingBatteryPercentage { + if let remainingBattery = deviceInformation.remainingBatteryPercentage { Section { ListRow("BATTERY") { BatteryIcon(percentage: Int(remainingBattery)) } } footer: { + // TODO: hint separate view! let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" Text("PROBLEMS_BATTERY_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Muse-Battery-Troubleshooting?language=\(locale.identifier))") } @@ -67,31 +70,36 @@ struct EEGDeviceDetails: View { @ViewBuilder private var headbandFit: some View { Section { ListRow("WEARING") { - if device.wearingHeadband { + if deviceInformation.wearingHeadband { Text("Yes") } else { Text("No") } } - if device.wearingHeadband { - ListRow("HEADBAND_FIT") { - let fit = device.fit.overallFit - Text(fit.localizedStringResource) - .foregroundStyle(fit.style) + if deviceInformation.wearingHeadband, + let fit = deviceInformation.fit { + ListRow("HEADBAND_FIT") { // TODO: detailed fit! + let overallFit = fit.overallFit + Text(overallFit.localizedStringResource) + .foregroundStyle(overallFit.style) } } } header: { Text("HEADBAND") } footer: { + // TODO: hint separate view! let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" Text("PROBLEMS_HEADBAND_FIT_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Sensor-Quality-Troubleshooting?language=\(locale.identifier))") } } - init(device: ConnectedDevice) { - self.device = device + init(model: String, state: ConnectionState, _ deviceInformation: MuseDeviceInformation, disconnect: @escaping () -> Void) { + self.model = model + self.state = state + self.deviceInformation = deviceInformation + self.disconnectClosure = disconnect } @@ -120,20 +128,45 @@ struct EEGDeviceDetails: View { #if DEBUG #Preview { - let model = EEGViewModel(mock: MockEEGDevice(name: "Mock Device", model: "Mock", state: .connected)) - return NavigationStack { - EEGDeviceDetails(device: model.activeDevice!) // swiftlint:disable:this force_unwrapping + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .connected, + .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", remainingBatteryPercentage: 75) + ) { + print("Disconnect Device") + } } } + #Preview { - let modelIntervention = EEGViewModel(mock: MockEEGDevice( - name: "Mock Device", - model: "Mock", - state: .interventionRequired("INTERVENTION_MUSE_FIRMWARE") - )) - return NavigationStack { - EEGDeviceDetails(device: modelIntervention.activeDevice!) // swiftlint:disable:this force_unwrapping + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .connected, + .init( + serialNumber: "0xAABBCCDD", + firmwareVersion: "1.0", + remainingBatteryPercentage: 75, + wearingHeadband: true, + fit: HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .good) + ) + ) { + print("Disconnect Device") + } + } +} + +#Preview { + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .interventionRequired("INTERVENTION_MUSE_FIRMWARE"), + .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", remainingBatteryPercentage: 75) + ) { + print("Disconnect Device") + } } } #endif diff --git a/NAMS/Devices/Muse/Views/MuseDeviceList.swift b/NAMS/Devices/Muse/Views/MuseDeviceList.swift new file mode 100644 index 0000000..a0f4545 --- /dev/null +++ b/NAMS/Devices/Muse/Views/MuseDeviceList.swift @@ -0,0 +1,41 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +#if MUSE +struct MuseDeviceList: View { + @Environment(MuseDeviceManager.self) + private var museDeviceManager + + var body: some View { + ForEach(museDeviceManager.nearbyMuses) { device in + MuseDeviceRow(device: device) + } + } + + + init() {} +} +#endif + + +/* + //TODO: move to mock preview? +#if DEBUG +#Preview { + NavigationStack { + List { + MuseDeviceList() + } + } + .environment(EEGViewModel(deviceManager: MockDeviceManager(immediate: true))) +} +#endif +*/ diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift new file mode 100644 index 0000000..c4f3f9c --- /dev/null +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -0,0 +1,94 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +#if MUSE +struct MuseDeviceRow: View { + private let device: MuseDevice + + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + + @State private var presentingActiveDevice: MuseDevice? + + var body: some View { + NearbyDeviceRow(peripheral: device) { + Task { + await deviceCoordinator.tapDevice(.muse(device)) + } + } secondaryAction: { + // TODO: we assume I button only shows if this is true! + if device.state == .connected { + presentingActiveDevice = device + } + } + .navigationDestination(item: $presentingActiveDevice) { device in + // TODO: should be always true??: Maybe just forward the optional (handles device disconnecting in the meantime!) + if let info = device.deviceInformation { + MuseDeviceDetailsView(model: device.model, state: device.connectionState, info) { + device.disconnect() + // TODO: reconsider this archotecture to catch external disconnects + deviceCoordinator.hintDisconnect() + } + } + } + } + + + init(device: MuseDevice) { // TODO: we could make this generic bluetooth peripheral? + self.device = device + } +} +#endif + + +/* +// TODO: replace within MockDeviceRow! +#if DEBUG +#Preview { + NavigationStack { + List { + // TODO : EEGDeviceRow(device: MockEEGDevice(name: "Nearby Device", model: "Mock")) + } + .environment(EEGViewModel(deviceManager: MockDeviceManager())) + } +} + +#Preview { + let device = MockDevice(name: "Device 1", model: "Mock", state: .connecting) + return NavigationStack { + List { + // TODO : EEGDeviceRow(device: device) + } + } + .environment(EEGViewModel(mock: device)) +} + +#Preview { + let device = MockDevice(name: "Device 2", model: "Mock", state: .connected) + return NavigationStack { + List { + // TODO : EEGDeviceRow(device: device) + } + .environment(EEGViewModel(mock: device)) + } +} + +#Preview { + let device = MockDevice(name: "Device 3", model: "Mock", state: .interventionRequired("Firmware update required.")) + return NavigationStack { + List { + // TODO : EEGDeviceRow(device: device) + } + } + .environment(EEGViewModel(mock: device)) +} +#endif +*/ diff --git a/NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift b/NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift new file mode 100644 index 0000000..f83df59 --- /dev/null +++ b/NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift @@ -0,0 +1,24 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseTroublesConnectingHint: View { + @Environment(\.locale) + private var locale + + var body: some View { + HStack { + let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" + Text("PROBLEMS_CONNECTING_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Bluetooth-Troubleshooting?language=\(locale.identifier))") + } + } + + init() {} +} diff --git a/NAMS/Devices/NearbyDevices.swift b/NAMS/Devices/NearbyDevices.swift deleted file mode 100644 index 7b3cf1e..0000000 --- a/NAMS/Devices/NearbyDevices.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct NearbyDevices: View { - private let eegModel: EEGViewModel - - @Environment(\.dismiss) - private var dismiss - @Environment(\.locale) - private var locale - - @State private var bluetoothManager = BluetoothManager() - - private var bluetoothPoweredOn: Bool { - if case .poweredOn = bluetoothManager.bluetoothState { - return true - } - #if targetEnvironment(simulator) - return true - #else - return ProcessInfo.processInfo.isPreviewSimulator - #endif - } - - var body: some View { - if bluetoothPoweredOn { - Text("TURN_ON_HEADBAND_HINT") - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: -15, leading: 0, bottom: 0, trailing: 0)) - .padding([.top, .leading, .trailing]) - } - - Section { - if bluetoothPoweredOn { - if eegModel.nearbyDevices.isEmpty { - ProgressView() - .frame(maxWidth: .infinity) - } else { - EEGDeviceList(eegModel: eegModel) - } - } - - bluetoothHints - } footer: { - sectionFooter - } - .onAppear { - onForeground() - } - .onDisappear { - onBackground() - } - .onReceive(NotificationCenter.default.publisher(for: UIScene.willEnterForegroundNotification)) { _ in - onForeground() // onAppear is coupled with view rendering only and won't get fired when putting app into the foreground - } - .onReceive(NotificationCenter.default.publisher(for: UIScene.willDeactivateNotification)) { _ in - onBackground() // onDisappear is coupled with view rendering only and won't get fired when putting app into the background - } - .onChange(of: bluetoothManager.bluetoothState) { - if case .poweredOn = bluetoothManager.bluetoothState { - eegModel.startScanning() - } else { - // this will still trigger an API MISUSE, both otherwise we end up in undefined state - eegModel.stopScanning(refreshNearby: bluetoothManager.bluetoothState == .poweredOn) - } - } - } - - @ViewBuilder var bluetoothHints: some View { - Group { - switch bluetoothManager.bluetoothState { - case .poweredOn: - EmptyView() - case .poweredOff: - VStack { - Text("BLUETOOTH_OFF") - .font(.title2) - .padding() - - Text("BLUETOOTH_OFF_HINT") - .multilineTextAlignment(.center) - } - case .unauthorized: - VStack { - Text("BLUETOOTH_PROHIBITED") - .font(.title2) - .padding() - - Text("BLUETOOTH_PROHIBITED_HINT") - .multilineTextAlignment(.center) - } - case .resetting, .unknown: - if !bluetoothPoweredOn { // preview case - Text("BLUETOOTH_UNKNOWN") - } - case .unsupported: - if !bluetoothPoweredOn { - Text("BLUETOOTH_UNSUPPORTED") - } - @unknown default: - EmptyView() - } - } - .listRowBackground(Color.clear) - } - - @ViewBuilder var sectionFooter: some View { - HStack { - let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" - Text("PROBLEMS_CONNECTING_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Bluetooth-Troubleshooting?language=\(locale.identifier))") - } - .padding(.top) - } - - - init(eegModel: EEGViewModel) { - self.eegModel = eegModel - } - - - @MainActor - func onForeground() { - if bluetoothPoweredOn { - eegModel.startScanning() - } - } - - @MainActor - func onBackground() { - eegModel.stopScanning(refreshNearby: bluetoothManager.bluetoothState == .poweredOn) - } -} - - -#if DEBUG -#Preview { - NavigationStack { - List { - NearbyDevices(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) - } - } -} - -#Preview { - NavigationStack { - List { - NearbyDevices(eegModel: EEGViewModel(deviceManager: MockDeviceManager(nearbyDevices: []))) - } - } -} -#endif diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift new file mode 100644 index 0000000..fdb74e9 --- /dev/null +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -0,0 +1,155 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Spezi +import SpeziBluetooth +import SwiftUI + + +struct NearbyDevicesView: View { + @Environment(Bluetooth.self) + private var bluetooth + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + + @Environment(BiopotDevice.self) + private var biopotDevice: BiopotDevice? +#if MUSE + @Environment(MuseDeviceManager.self) + private var museDeviceManager +#endif + @Environment(MockDeviceManager.self) + private var mockDeviceManager: MockDeviceManager? // TODO: flag to disable mock devices! + + @Environment(\.dismiss) + private var dismiss + + // TODO: implement! + @AppStorage(StorageKeys.autoConnect) + private var autoConnect = true + @AppStorage(StorageKeys.autoConnectBackground) + private var autoConnectBackground = false + + + private var consideredPoweredOn: Bool { + mockDeviceManager != nil || bluetooth.state == .poweredOn + } + + + private var isScanning: Bool { + mockDeviceManager != nil || bluetooth.isScanning + } + + var body: some View { + // TODO: remove closure length! + // swiftlint:disable:next closure_body_length + NavigationStack { + List { // swiftlint:disable:this closure_body_length + Section { + Toggle("Auto Connect", isOn: $autoConnect) + if autoConnect { + Toggle("Auto Connect Background", isOn: $autoConnectBackground) // TODO: make it a selection navigation destination? + } + } + + if consideredPoweredOn { + Section { // TODO: think about this placement? + Text("TURN_ON_HEADBAND_HINT") + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(Color.clear) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } + + // TODO: sort all devices by initial discovery (descending?, latest at the top!) + Section { + #if MUSE + MuseDeviceList() + #endif + + let biopots = bluetooth.nearbyDevices(for: BiopotDevice.self) + ForEach(biopots) { biopot in + BiopotDeviceRow(device: biopot) + } + + if let mockDeviceManager { + ForEach(mockDeviceManager.nearbyDevices) { device in + MockDeviceRow(device: device) + } + } + } header: { + LoadingSectionHeader("Devices", loading: isScanning) + } footer: { + MuseTroublesConnectingHint() + } + } else { + Section { + BluetoothStateHints(state: bluetooth.state) + } + } + } + .navigationTitle("NEARBY_DEVICES") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + Button("Close") { + dismiss() + } + } + } + // TODO: this probably conflicts with a global .autoConnect modifier! + // TODO: auto-connect also conflicts with Muse devices? (enough to disable autoConnect if we have something connected?) + .scanNearbyDevices(with: bluetooth, autoConnect: false) // TODO: allow to dynamically disable autoConnect! + // TODO: how to handle optional modifiers? + .scanNearbyDevices(enabled: mockDeviceManager != nil, with: mockDeviceManager ?? MockDeviceManager()) +#if MUSE + .scanNearbyDevices(enabled: bluetooth.state == .poweredOn, with: museDeviceManager) + .onChange(of: bluetooth.state) { + if case .poweredOn = bluetooth.state { + museDeviceManager.startScanning() + } else { + // this will still trigger an API MISUSE, but otherwise we end up in undefined state + // TODO: museDeviceManager.stopScanning() + } + } +#endif + } + + + init() {} +} + + +#if DEBUG +#Preview { + NearbyDevicesView() + .previewWith { + DeviceCoordinator() + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } +#if MUSE + .environment(MuseDeviceManager()) // TODO: make this a module? +#endif + .environment(MockDeviceManager()) +} + +#Preview { + NearbyDevicesView() + .previewWith { + DeviceCoordinator() + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + } + } +#if MUSE + .environment(MuseDeviceManager()) // TODO: make this a module? +#endif + .environment(MockDeviceManager()) +} +#endif diff --git a/NAMS/EEG/Chart/EEGChannelMark.swift b/NAMS/EEG/Chart/EEGChannelMark.swift index a38f8ff..48fd252 100644 --- a/NAMS/EEG/Chart/EEGChannelMark.swift +++ b/NAMS/EEG/Chart/EEGChannelMark.swift @@ -33,7 +33,7 @@ struct EEGChannelMark: ChartContent { #if DEBUG #Preview { - let randomSamples = EEGMeasurementGenerator(sampleRate: 60) + let randomSamples = MockMeasurementGenerator(sampleRate: 60) let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10) return EEGChart(measurements: generated.data.suffix(from: 0), for: .af7, baseTime: generated.baseTime) } diff --git a/NAMS/EEG/Chart/EEGChart.swift b/NAMS/EEG/Chart/EEGChart.swift index 8fdfa16..eb27bd9 100644 --- a/NAMS/EEG/Chart/EEGChart.swift +++ b/NAMS/EEG/Chart/EEGChart.swift @@ -81,7 +81,7 @@ struct EEGChart: View { #if DEBUG -private let randomSamples = EEGMeasurementGenerator(sampleRate: 60) +private let randomSamples = MockMeasurementGenerator(sampleRate: 60) private let generated = randomSamples.generateRecording(sampleTime: 5, recordingOffset: 10) #Preview { diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index 103ecf1..44c7a85 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -7,6 +7,7 @@ // import Charts +import Spezi import SpeziBluetooth import SpeziOnboarding import SpeziViews @@ -17,9 +18,10 @@ struct EEGRecording: View { @Environment(\.dismiss) private var dismiss - private let eegModel: EEGViewModel - @Environment(BiopotDevice.self) - private var biopot: BiopotDevice? + @Environment(EEGRecordings.self) + private var eegModel + @Environment(DeviceCoordinator.self) + private var deviceCoordinator @Environment(PatientListModel.self) private var patientList @@ -32,7 +34,7 @@ struct EEGRecording: View { var body: some View { ZStack { - if !(biopot?.connected ?? false) && eegModel.activeDevice == nil { + if !deviceCoordinator.isConnected { NoInformationText { Text("No Device connected!") } caption: { @@ -60,9 +62,18 @@ struct EEGRecording: View { .navigationTitle("EEG Recording") .navigationBarTitleDisplayMode(.inline) } else { - StartRecordingView(eegModel: eegModel) + StartRecordingView() } } + .onAppear { + if case .muse = deviceCoordinator.connectedDevice { + frequency = .theta + } + } + .onDisappear { + // TODO: discarding confirmation? + eegModel.stopRecordingSession() + } .toolbar { Button("Close") { dismiss() @@ -85,9 +96,7 @@ struct EEGRecording: View { } } - init(eegModel: EEGViewModel) { - self.eegModel = eegModel - } + init() {} @ViewBuilder @@ -112,14 +121,17 @@ struct EEGRecording: View { #if DEBUG -#Preview { - let device = MockEEGDevice(name: "Device 1", model: "Mock", state: .connected) - let model = EEGViewModel(mock: device) - model.startRecordingSession() +#Preview { // TODO: verify previews + let model = EEGRecordings() + Task { @MainActor in + try await model.startRecordingSession() + } return NavigationStack { - EEGRecording(eegModel: model) + EEGRecording() .environment(PatientListModel()) + .environment(model) .previewWith { + DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) } @@ -128,26 +140,23 @@ struct EEGRecording: View { } #Preview { - let device = MockEEGDevice(name: "Device 1", model: "Mock", state: .connected) - return NavigationStack { - EEGRecording(eegModel: EEGViewModel(mock: device)) + NavigationStack { + EEGRecording() .environment(PatientListModel()) + .environment(EEGRecordings()) .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } + DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) } } } #Preview { NavigationStack { - EEGRecording(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) + EEGRecording() .environment(PatientListModel()) + .environment(EEGRecordings()) .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } + DeviceCoordinator() } } } diff --git a/NAMS/EEG/Chart/StartRecordingView.swift b/NAMS/EEG/Chart/StartRecordingView.swift index 89cc871..45b88cd 100644 --- a/NAMS/EEG/Chart/StartRecordingView.swift +++ b/NAMS/EEG/Chart/StartRecordingView.swift @@ -12,9 +12,8 @@ import SwiftUI struct StartRecordingView: View { - private let eegModel: EEGViewModel - @Environment(BiopotDevice.self) - private var biopot: BiopotDevice? + @Environment(EEGRecordings.self) + private var eegModel var body: some View { OnboardingView( @@ -42,28 +41,23 @@ struct StartRecordingView: View { ], actionText: "Start Recording", action: { - eegModel.startRecordingSession() - if let biopot, biopot.connected { - Task { - await biopot.enableRecording() - } - } + try await eegModel.startRecordingSession() } ) - .tint(.pink) + .tint(.pink) } - init(eegModel: EEGViewModel) { - self.eegModel = eegModel + init() { } } #if DEBUG #Preview { - StartRecordingView(eegModel: EEGViewModel(mock: MockEEGDevice(name: "Device 1", model: "Mock", state: .connected))) + StartRecordingView() .previewWith { + EEGRecordings() Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) } diff --git a/NAMS/EEG/EEGRecordingSession.swift b/NAMS/EEG/EEGRecordingSession.swift index 381c94a..bd961a2 100644 --- a/NAMS/EEG/EEGRecordingSession.swift +++ b/NAMS/EEG/EEGRecordingSession.swift @@ -11,5 +11,16 @@ import Foundation @Observable class EEGRecordingSession { - var measurements: [EEGFrequency: [EEGSeries]] = [:] + private(set) var measurements: [EEGFrequency: [EEGSeries]] = [:] + + + func append(series: EEGSeries, for frequency: EEGFrequency) { + measurements[frequency, default: []] + .append(series) + } + + func append(series: [EEGSeries], for frequency: EEGFrequency) { + measurements[frequency, default: []] + .append(contentsOf: series) + } } diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift new file mode 100644 index 0000000..f381026 --- /dev/null +++ b/NAMS/EEG/EEGRecordings.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import Spezi + + +// TODO: search and replace .environment(EEGRecordings()) + +@Observable +class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { + let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseViewModel") + + @Dependency @ObservationIgnored private var deviceCoordinator: DeviceCoordinator + + private(set) var recordingSession: EEGRecordingSession? + + required init() {} + + @MainActor + func startRecordingSession() async throws { + let session = EEGRecordingSession() + self.recordingSession = session + + guard let device = deviceCoordinator.connectedDevice else { + // TODO: throw an error! + logger.error("Tried to start EEG recording but no connected device was found!") + return + } + + // TODO: get current device and enable recording session? on device coordinator (so they can handle changing devices?) + // TODO: handle the case where the device disconnects when an ongoing recording is in progress? + try await device.startRecording(session) + } + + @MainActor + func stopRecordingSession() { + self.recordingSession = nil + if let device = deviceCoordinator.connectedDevice { + device.stopRecording() // TODO async? + } + } +} diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 4efc9eb..5a7c328 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -18,7 +18,9 @@ struct HomeView: View { case contact case mockUpload } - + + // TODO: EEGViewModel should be here? + @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule @AppStorage(StorageKeys.selectedPatient) @@ -29,6 +31,11 @@ struct HomeView: View { @Environment(BiopotDevice.self) private var biopot: BiopotDevice? + @State var mockDeviceManager = MockDeviceManager() +#if MUSE + @State var museDeviceManager = MuseDeviceManager() +#endif + @State private var patientList = PatientListModel() @State private var viewState: ViewState = .idle @@ -48,6 +55,10 @@ struct HomeView: View { } } .environment(patientList) + .environment(mockDeviceManager) +#if MUSE + .environment(museDeviceManager) +#endif .viewStateAlert(state: $viewState) .onAppear { if FeatureFlags.injectDefaultPatient { @@ -106,6 +117,8 @@ struct HomeView: View { return HomeView() .previewWith { + DeviceCoordinator() + EEGRecordings() AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) } } diff --git a/NAMS/NAMSAppDelegate.swift b/NAMS/NAMSAppDelegate.swift index b085140..5995d56 100644 --- a/NAMS/NAMSAppDelegate.swift +++ b/NAMS/NAMSAppDelegate.swift @@ -34,6 +34,9 @@ class NAMSAppDelegate: SpeziAppDelegate { } firestore + DeviceCoordinator() + EEGRecordings() + Bluetooth { // TODO: can this be based on the type of BiopotDevice service property? Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index 13ac029..8e739fd 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -30,6 +30,9 @@ } } } + }, + "..." : { + }, "%@ " : { "comment" : "Image prefix placeholder", @@ -141,6 +144,7 @@ } }, "ATTENTION_REQUIRED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -149,6 +153,12 @@ } } } + }, + "Auto Connect" : { + + }, + "Auto Connect Background" : { + }, "BATTERY" : { "localizations" : { @@ -169,9 +179,6 @@ } } } - }, - "Biopot" : { - }, "Bluetooth Off" : { @@ -186,6 +193,7 @@ }, "BLUETOOTH_OFF" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -200,12 +208,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth is turned off. Please turn on Bluetooth in Control Center or Settings in order to use your Muse Headband." + "value" : "Bluetooth is turned off. Please turn on Bluetooth in Control Center or Settings, in order to connect to a nearby device." } } } }, "BLUETOOTH_PROHIBITED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -220,7 +229,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth is required to make connections to your Muse Headband. Please allow Bluetooth connections in your Privacy settings." + "value" : "Bluetooth is required to make connections to a nearby device. Please allow Bluetooth connections in your Privacy settings." } } } @@ -230,7 +239,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "We have troubles with the Bluetooth communication. Please try again." + "value" : "We have troubles with the Bluetooth communication.\\n\nPlease try again." } } } @@ -386,9 +395,6 @@ }, "Device" : { - }, - "Device Type" : { - }, "DEVICE_DETAILS" : { "localizations" : { @@ -522,6 +528,7 @@ } }, "Firmware update required." : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -617,6 +624,9 @@ } } } + }, + "Intervention Required" : { + }, "INTERVENTION_MUSE_FIRMWARE" : { "localizations" : { @@ -765,9 +775,6 @@ } } } - }, - "Muse" : { - }, "Name" : { "localizations" : { @@ -1020,6 +1027,9 @@ } } } + }, + "Requires Attention" : { + }, "Reset" : { "extractionState" : "stale", diff --git a/NAMS/Resources/SocialSupportQuestionnaire.json b/NAMS/Resources/SocialSupportQuestionnaire.json deleted file mode 100644 index 8a79098..0000000 --- a/NAMS/Resources/SocialSupportQuestionnaire.json +++ /dev/null @@ -1,387 +0,0 @@ -{ - "resourceType": "Questionnaire", - "language": "en-US", - "id": "socialsupport", - "name": "SocialSupport", - "title": "Social Support", - "description": "This survey measures tangible social support plus a couple of demographic questions.", - "version": "1", - "status": "draft", - "publisher": "RAND Corp", - "meta": { - "profile": [ - "http://spezi.stanford.edu/fhir/StructureDefinition/sdf-Questionnaire" - ], - "tag": [ - { - "system": "urn:ietf:bcp:47", - "code": "en-US", - "display": "English" - } - ] - }, - "useContext": [ - { - "code": { - "system": "http://hl7.org/fhir/ValueSet/usage-context-type", - "code": "focus", - "display": "Clinical Focus" - }, - "valueCodeableConcept": { - "coding": [ - { - "system": "urn:oid:2.16.578.1.12.4.1.1.8655", - "display": "Social Support" - } - ] - } - } - ], - "contact": [ - { - "name": "https://www.rand.org/health-care/surveys_tools/mos/social-support/survey-instrument.html" - } - ], - "subjectType": [ - "Patient" - ], - "purpose": "The RAND Medical Outcomes Social Support survey is a 4-item questionnaire that measures social support.", - "copyright": "RAND Corp surveys are open-source and free to use.", - "date": "2023-01-23T00:00:00-08:00", - "url": "http://spezi.stanford.edu/fhir/questionnaire/32f43c8e-93e9-4c70-97a0-e716f8030073", - "item": [ - { - "linkId": "dcea2683-9815-4505-b240-e75b502b29ef", - "type": "choice", - "text": "How often do you need someone to help you if you were confined to bed?", - "required": false, - "answerOption": [ - { - "valueCoding": { - "id": "3d6fe1b8-c64b-497c-8583-db7ddda9e94e", - "code": "1", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "None of the time" - } - }, - { - "valueCoding": { - "id": "b4081e9d-d0f1-4aea-9a15-eac4a15d1d10", - "code": "2", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "A little of the time" - } - }, - { - "valueCoding": { - "id": "e32f7952-e280-48d7-9746-c13dbb26638f", - "code": "3", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Some of the time" - } - }, - { - "valueCoding": { - "id": "d2f6172d-9402-4cb3-870a-584a7be3a5d7", - "code": "4", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Most of the time" - } - }, - { - "valueCoding": { - "id": "ec48001e-f03e-4a14-8a7a-9fcf34fa81d2", - "code": "5", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "All of the time" - } - } - ] - }, - { - "linkId": "ce09d701-7b93-4150-defb-51825e05ade9", - "type": "choice", - "text": "How often do you need someone to take you to the doctor if you needed it?", - "required": false, - "answerOption": [ - { - "valueCoding": { - "id": "3d6fe1b8-c64b-497c-8583-db7ddda9e94e", - "code": "1", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "None of the time" - } - }, - { - "valueCoding": { - "id": "b4081e9d-d0f1-4aea-9a15-eac4a15d1d10", - "code": "2", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "A little of the time" - } - }, - { - "valueCoding": { - "id": "e32f7952-e280-48d7-9746-c13dbb26638f", - "code": "3", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Some of the time" - } - }, - { - "valueCoding": { - "id": "d2f6172d-9402-4cb3-870a-584a7be3a5d7", - "code": "4", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Most of the time" - } - }, - { - "valueCoding": { - "id": "ec48001e-f03e-4a14-8a7a-9fcf34fa81d2", - "code": "5", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "All of the time" - } - } - ] - }, - { - "linkId": "58e97564-5f4d-4d4b-86d5-6429cbbc7a8e", - "type": "choice", - "text": "How often do you need someone to prepare your meals if you were unable to do it yourself?", - "required": false, - "answerOption": [ - { - "valueCoding": { - "id": "3d6fe1b8-c64b-497c-8583-db7ddda9e94e", - "code": "1", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "None of the time" - } - }, - { - "valueCoding": { - "id": "b4081e9d-d0f1-4aea-9a15-eac4a15d1d10", - "code": "2", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "A little of the time" - } - }, - { - "valueCoding": { - "id": "e32f7952-e280-48d7-9746-c13dbb26638f", - "code": "3", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Some of the time" - } - }, - { - "valueCoding": { - "id": "d2f6172d-9402-4cb3-870a-584a7be3a5d7", - "code": "4", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Most of the time" - } - }, - { - "valueCoding": { - "id": "ec48001e-f03e-4a14-8a7a-9fcf34fa81d2", - "code": "5", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "All of the time" - } - } - ] - }, - { - "linkId": "ad161c49-e8a6-4d31-90e8-02b2887a765f", - "type": "choice", - "text": "How often do you need someone to help with daily chores if you were sick", - "required": false, - "answerOption": [ - { - "valueCoding": { - "id": "3d6fe1b8-c64b-497c-8583-db7ddda9e94e", - "code": "1", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "None of the time" - } - }, - { - "valueCoding": { - "id": "b4081e9d-d0f1-4aea-9a15-eac4a15d1d10", - "code": "2", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "A little of the time" - } - }, - { - "valueCoding": { - "id": "e32f7952-e280-48d7-9746-c13dbb26638f", - "code": "3", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Some of the time" - } - }, - { - "valueCoding": { - "id": "d2f6172d-9402-4cb3-870a-584a7be3a5d7", - "code": "4", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "Most of the time" - } - }, - { - "valueCoding": { - "id": "ec48001e-f03e-4a14-8a7a-9fcf34fa81d2", - "code": "5", - "system": "urn:uuid:e9ecdd47-2e8b-49b3-8780-9d0769a246aa", - "display": "All of the time" - } - } - ] - }, - { - "linkId": "ba518851-2843-4bbd-c0f7-5b5692d542e0", - "type": "integer", - "text": "What is your age?", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/minValue", - "valueInteger": 18 - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/maxValue", - "valueInteger": 120 - }, - { - "url": "http://biodesign.stanford.edu/fhir/StructureDefinition/validationtext", - "valueString": "Please enter a valid age." - } - ], - "required": false - }, - { - "linkId": "695525f3-3e89-4455-8e25-878171c596da", - "type": "choice", - "text": "What is your preferred contact method?", - "required": false, - "answerOption": [ - { - "valueCoding": { - "id": "b7a3d7a5-52b9-49b1-8b59-7a3885483f1c", - "code": "phone-call", - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "display": "Phone call" - } - }, - { - "valueCoding": { - "id": "3d42dde0-8e60-4832-bd46-bd06de28cbf2", - "code": "text-message", - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "display": "Text message" - } - }, - { - "valueCoding": { - "id": "e672cfc6-118f-4a2d-aafd-02722ff876b9", - "code": "e-mail", - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "display": "E-mail" - } - } - ] - }, - { - "linkId": "c3bea33d-4c50-4f4a-8ae4-1a52be326b19", - "type": "string", - "text": "What is your phone number? Ex. (555) 555-5555", - "required": false, - "enableWhen": [ - { - "question": "695525f3-3e89-4455-8e25-878171c596da", - "operator": "=", - "answerCoding": { - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "code": "phone-call" - } - } - ], - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/regex", - "valueString": "^(\\([0-9]{3}\\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$" - }, - { - "url": "http://biodesign.stanford.edu/fhir/StructureDefinition/validationtext", - "valueString": "Please enter a valid phone number." - } - ] - }, - { - "linkId": "8e906a39-5fd0-42a8-f42c-bd96d719dd13", - "type": "string", - "text": "What is your text number? Ex. (555) 555-5555", - "required": false, - "enableWhen": [ - { - "question": "695525f3-3e89-4455-8e25-878171c596da", - "operator": "=", - "answerCoding": { - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "code": "text-message" - } - } - ], - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/regex", - "valueString": "^(\\([0-9]{3}\\) |[0-9]{3}-)[0-9]{3}-[0-9]{4}$" - }, - { - "url": "http://biodesign.stanford.edu/fhir/StructureDefinition/validationtext", - "valueString": "Please enter a valid phone number." - } - ] - }, - { - "linkId": "86290b0a-017e-4193-8707-dc0c2146f0eb", - "type": "string", - "text": "What is your e-mail?", - "extension": [ - { - "url": "http://hl7.org/fhir/StructureDefinition/regex", - "valueString": ".*@.+" - }, - { - "url": "http://biodesign.stanford.edu/fhir/StructureDefinition/validationtext", - "valueString": "Please enter a valid email" - }, - { - "url": "http://hl7.org/fhir/StructureDefinition/minLength", - "valueInteger": 1 - } - ], - "required": false, - "maxLength": 50, - "enableWhen": [ - { - "question": "695525f3-3e89-4455-8e25-878171c596da", - "operator": "=", - "answerCoding": { - "system": "urn:uuid:736ac230-812a-4f4a-edec-5156910fb6ec", - "code": "e-mail" - } - } - ] - }, - { - "linkId": "305f5381-2d8b-4b98-bc04-5a39bee2f7ec", - "type": "display", - "text": "Thank you for taking the survey!", - "required": false - } - ] -} \ No newline at end of file diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index a19a0c9..3055370 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -12,11 +12,6 @@ import SwiftUI struct ScheduleView: View { -#if MUSE - @State var eegModel = EEGViewModel(deviceManager: MuseDeviceManager()) -#else - @State var eegModel = EEGViewModel(deviceManager: MockDeviceManager()) -#endif @Environment(BiopotDevice.self) private var biopot: BiopotDevice? @@ -45,18 +40,18 @@ struct ScheduleView: View { .padding() } } else { - TilesView(eegModel: eegModel) + TilesView() } } .navigationTitle(Text("Schedule", comment: "Schedule Title")) .sheet(isPresented: $presentingMuseList) { - DevicesSheet(eegModel: eegModel) + NearbyDevicesView() } .sheet(isPresented: $presentPatientSheet) { PatientListSheet(activePatientId: $activePatientId) } .onAppear { - biopot?.associate(eegModel) // TODO: that has to change! + // TODO: biopot?.associate(eegModel) // TODO: that has to change! } .toolbar { toolbar @@ -98,7 +93,9 @@ struct ScheduleView: View { #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant(nil)) .environment(PatientListModel()) + .environment(EEGRecordings()) .previewWith { + DeviceCoordinator() Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) } @@ -111,7 +108,9 @@ struct ScheduleView: View { #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1")) .environment(PatientListModel()) + .environment(EEGRecordings()) .previewWith { + DeviceCoordinator() Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) } diff --git a/NAMS/Tiles/MeasurementTile.swift b/NAMS/Tiles/MeasurementTile.swift index d6d0009..db2e542 100644 --- a/NAMS/Tiles/MeasurementTile.swift +++ b/NAMS/Tiles/MeasurementTile.swift @@ -39,9 +39,11 @@ struct MeasurementTile: View { Text(task.title) .font(.title) .fontWeight(.semibold) + .multilineTextAlignment(.center) // works better for larger text sizes Text("\(task.expectedCompletionMinutes) min") .foregroundColor(.secondary) .font(.subheadline) + .multilineTextAlignment(.center) } footer: { tileDescription } diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index 08c06cb..4535865 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziBluetooth import SpeziQuestionnaire import SpeziViews @@ -16,10 +17,8 @@ extension Questionnaire: Identifiable {} // TODO: move somewhere! @MainActor struct TilesView: View { - private let eegModel: EEGViewModel - - @Environment(BiopotDevice.self) - private var biopot: BiopotDevice? + @Environment(DeviceCoordinator.self) + private var deviceCoordinator @Environment(PatientListModel.self) private var patientList @@ -47,7 +46,7 @@ struct TilesView: View { MeasurementTile( task: measurement, presentingEEGRecording: $presentingEEGRecording, - deviceConnected: eegModel.activeDevice != nil || biopot?.connected == true + deviceConnected: deviceCoordinator.isConnected ) } } @@ -77,16 +76,14 @@ struct TilesView: View { } .sheet(isPresented: $presentingEEGRecording) { NavigationStack { - EEGRecording(eegModel: eegModel) + EEGRecording() } } } } - init(eegModel: EEGViewModel) { - self.eegModel = eegModel - } + init() {} private func taskList(_ tasks: [T]) -> [T] { @@ -105,22 +102,31 @@ struct TilesView: View { #Preview { let patientList = PatientListModel() patientList.completedTasks = [] - return TilesView(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) + return TilesView() .environment(patientList) + .environment(EEGRecordings()) .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } + DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) + } +} + +#Preview { + let patientList = PatientListModel() + patientList.completedTasks = [] + return TilesView() + .environment(patientList) + .environment(EEGRecordings()) + .previewWith { + DeviceCoordinator() } } #Preview { - TilesView(eegModel: EEGViewModel(deviceManager: MockDeviceManager())) + TilesView() .environment(PatientListModel()) + .environment(EEGRecordings()) .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } + DeviceCoordinator() } } #endif diff --git a/NAMS/Utils/ListRow.swift b/NAMS/Utils/ListRow.swift index 98a1cd5..844719f 100644 --- a/NAMS/Utils/ListRow.swift +++ b/NAMS/Utils/ListRow.swift @@ -8,30 +8,120 @@ import SwiftUI +public enum Alignment { // TODO: better name? + case horizontal + case vertical +} + +extension Alignment: PreferenceKey { + public typealias Value = Self? + + public static func reduce(value: inout Self?, nextValue: () -> Self?) { + if let nextValue = nextValue() { + value = nextValue + } + } +} + + +public struct DynamicHStack: View { // TODO: move to Spezi Views + private let realignAfter: DynamicTypeSize + private let verticalAlignment: VerticalAlignment + private let horizontalAlignment: HorizontalAlignment + private let spacing: CGFloat? + private let content: Content + + @Environment(\.dynamicTypeSize) + private var dynamicTypeSize + + + public var body: some View { + if dynamicTypeSize <= realignAfter { + HStack(alignment: verticalAlignment, spacing: spacing) { + content + } + .preference(key: Alignment.self, value: .horizontal) + } else { + VStack(alignment: horizontalAlignment, spacing: spacing) { + content + } + .preference(key: Alignment.self, value: .vertical) + } + } + + + public init( + realignAfter: DynamicTypeSize = .xxLarge, + verticalAlignment: VerticalAlignment = .center, + horizontalAlignment: HorizontalAlignment = .center, + spacing: CGFloat? = nil, + @ViewBuilder content: () -> Content + ) { + self.realignAfter = realignAfter + self.verticalAlignment = verticalAlignment + self.horizontalAlignment = horizontalAlignment + self.spacing = spacing + self.content = content() + } +} + + +public struct ListRow: View { + private let label: Label + private let content: Content + -struct ListRow: View { - private let name: Text - private let value: Value + @Environment(\.dynamicTypeSize) + private var dynamicTypeSize + @State private var alignment: Alignment? - var body: some View { + + public var body: some View { HStack { - name - Spacer() - value - .foregroundColor(.secondary) + DynamicHStack(horizontalAlignment: .leading) { + label + .foregroundColor(.primary) + .lineLimit(alignment == .horizontal ? 1 : nil) + + if alignment == .horizontal { + Spacer() + } + + content + .lineLimit(alignment == .horizontal ? 1 : nil) + .layoutPriority(1) + .foregroundColor(.secondary) + } + + if alignment == .vertical { + Spacer() + } } .accessibilityElement(children: .combine) + .onPreferenceChange(Alignment.self) { value in + alignment = value + } + } + + + public init(verbatim label: String, @ViewBuilder content: () -> Content) where Label == Text { + self.init(label, content: content) } @_disfavoredOverload - init(_ string: String, @ViewBuilder value: () -> Value) { - self.name = Text(verbatim: string) - self.value = value() + public init(_ label: String, @ViewBuilder content: () -> Content) where Label == Text { + self.init({ Text(verbatim: label) }, content: content) } - init(_ name: LocalizedStringResource, @ViewBuilder value: () -> Value) { - self.name = Text(name) - self.value = value() + public init(_ label: LocalizedStringResource, @ViewBuilder content: () -> Content) where Label == Text { + self.init({ Text(label) }, content: content) + } + + + // TODO: make arbitrary label view! + public init(@ViewBuilder _ label: () -> Label, @ViewBuilder content: () -> Content) { + self.label = label() + self.content = content() } } @@ -39,9 +129,32 @@ struct ListRow: View { #if DEBUG #Preview { List { - ListRow("Hello") { + ListRow(verbatim: "Hello") { Text(verbatim: "World") } + + HStack { + ListRow(verbatim: "Device") { + EmptyView() + } + ProgressView() + } + + HStack { + ListRow(verbatim: "Device") { + Text(verbatim: "World") + } + ProgressView() + .padding(.leading, 6) + } + + HStack { + ListRow(verbatim: "Long Device Name") { + Text(verbatim: "Long Description") + } + ProgressView() + .padding(.leading, 4) + } } } #endif diff --git a/NAMS/Utils/NoInformationText.swift b/NAMS/Utils/NoInformationText.swift index c94de50..5ae3e92 100644 --- a/NAMS/Utils/NoInformationText.swift +++ b/NAMS/Utils/NoInformationText.swift @@ -14,7 +14,7 @@ struct NoInformationText: View { private let header: Header private let caption: Caption - var body: some View { + var body: some View { // TODO: verify with large text and move to SpeziViews? VStack { header .font(.title2) diff --git a/NAMS/Utils/StorageKeys.swift b/NAMS/Utils/StorageKeys.swift index 91823a6..1b8a771 100644 --- a/NAMS/Utils/StorageKeys.swift +++ b/NAMS/Utils/StorageKeys.swift @@ -20,4 +20,8 @@ enum StorageKeys { static let homeTabSelection = "home.tabselection" /// The currently selected patient. static let selectedPatient = "active.patient" + + // MARK: - Nearby Devices + static let autoConnect = "bluetooth.auto-connect" + static let autoConnectBackground = "bluetooth.auto-connect.background" } From 1df0fe883f06d5e1157eeabb02dd90182386077f Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Mon, 29 Jan 2024 15:55:22 -0800 Subject: [PATCH 04/21] Accessibility imprvoements, UI refactorings, some progress --- NAMS.xcodeproj/project.pbxproj | 112 ++++++++-- NAMS/Devices/BioPot/Biopot.swift | 194 ------------------ .../{BioPot => Biopot}/BiopotDevice.swift | 9 +- .../Characteristics/AccelerometerSample.swift | 0 .../Characteristics/ByteBuffer+Int24.swift | 0 .../Characteristics/DataAcquisition.swift | 0 .../Characteristics/DataControl.swift | 0 .../Characteristics/DeviceConfiguration.swift | 0 .../Characteristics/DeviceInformation.swift | 0 .../Characteristics/EEGSample.swift | 0 .../ImpedanceMeasurement.swift | 0 .../SamplingConfiguration.swift | 0 .../Recording/EEGChannel+Biopot.swift | 0 .../Views/BiopotDeviceDetailsView.swift | 15 +- .../Views/BiopotDeviceRow.swift | 6 +- NAMS/Devices/DeviceCoordinator.swift | 8 +- .../MaybeExternal/NearbyDeviceRow.swift | 17 +- NAMS/Devices/Mock/MockDevice.swift | 9 +- NAMS/Devices/Mock/MockDeviceManager.swift | 16 +- NAMS/Devices/Mock/Views/MockDeviceRow.swift | 26 ++- .../IXNMuseConfiguration+Description.swift | 34 +-- .../Extensions/IXNMuseVersion+String.swift | 4 +- NAMS/Devices/Muse/Model/ConnectionState.swift | 19 -- NAMS/Devices/Muse/Model/Fit.swift | 19 +- NAMS/Devices/Muse/MuseDevice.swift | 32 +-- NAMS/Devices/Muse/MuseDeviceManager.swift | 2 +- .../Devices/Muse/Views/Details/FitLabel.swift | 48 +++++ .../Details/MuseAboutDetailsSection.swift | 45 ++++ .../Details/MuseBatteryDetailsSection.swift | 42 ++++ .../Views/Details/MuseDeviceDetailsView.swift | 116 +++++++++++ .../Details/MuseHeadbandFitSection.swift | 94 +++++++++ .../Views/Details/MuseHeadbandFitView.swift | 56 +++++ .../Views/Hints/MuseBatteryProblemsHint.swift | 31 +++ .../MuseConnectingProblemsHint.swift} | 7 + .../Hints/MuseHeadbandFitProblemsHint.swift | 31 +++ .../Hints/MuseInterventionRequiredHint.swift | 46 +++++ .../Muse/Views/MuseDeviceDetailsView.swift | 172 ---------------- NAMS/Devices/Muse/Views/MuseDeviceList.swift | 15 -- NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 63 +----- NAMS/EEG/Chart/EEGRecording.swift | 6 +- NAMS/EEG/EEGRecordings.swift | 6 +- NAMS/Home.swift | 3 +- NAMS/Resources/Localizable.xcstrings | 90 ++++---- NAMS/ScheduleView.swift | 3 - NAMS/Tiles/ScreeningTile.swift | 28 +-- NAMS/Tiles/ScreeningTileHeader.swift | 80 ++++++++ NAMS/Tiles/TilesView.swift | 2 - NAMS/Utils/ListRow.swift | 3 +- NAMS/Utils/NoInformationText.swift | 19 +- NAMS/Utils/Questionnaire+Identifiable.swift | 12 ++ 50 files changed, 881 insertions(+), 659 deletions(-) delete mode 100644 NAMS/Devices/BioPot/Biopot.swift rename NAMS/Devices/{BioPot => Biopot}/BiopotDevice.swift (95%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/AccelerometerSample.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/ByteBuffer+Int24.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/DataAcquisition.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/DataControl.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/DeviceConfiguration.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/DeviceInformation.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/EEGSample.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/ImpedanceMeasurement.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Characteristics/SamplingConfiguration.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Recording/EEGChannel+Biopot.swift (100%) rename NAMS/Devices/{BioPot => Biopot}/Views/BiopotDeviceDetailsView.swift (78%) rename NAMS/Devices/{BioPot => Biopot}/Views/BiopotDeviceRow.swift (85%) create mode 100644 NAMS/Devices/Muse/Views/Details/FitLabel.swift create mode 100644 NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift create mode 100644 NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift create mode 100644 NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift create mode 100644 NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift create mode 100644 NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift create mode 100644 NAMS/Devices/Muse/Views/Hints/MuseBatteryProblemsHint.swift rename NAMS/Devices/Muse/Views/{MuseTroublesConnectingHint.swift => Hints/MuseConnectingProblemsHint.swift} (90%) create mode 100644 NAMS/Devices/Muse/Views/Hints/MuseHeadbandFitProblemsHint.swift create mode 100644 NAMS/Devices/Muse/Views/Hints/MuseInterventionRequiredHint.swift delete mode 100644 NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift create mode 100644 NAMS/Tiles/ScreeningTileHeader.swift create mode 100644 NAMS/Utils/Questionnaire+Identifiable.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 11fb20f..ef86166 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; 2DC172E36CAF9AA8F191F723 /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; + 2DC172F25F327CB4B718D589 /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; 2DC17337BA4FEF5664BC0D10 /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; @@ -57,8 +58,7 @@ 2DC17CD906FF31BA6EA4CACD /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; - 2DC17E464BAEACF3A2B554B5 /* MuseTroublesConnectingHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */; }; - 2DC17F033D0282AEE8945A47 /* MuseTroublesConnectingHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */; }; + 2DC17E6D396A8A2B50E1D44F /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FE89AC206431F90166 /* PatientInformation.swift */; }; 2DC17F8981E53AEED0CFD1BD /* MockDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockDevice.swift */; }; @@ -163,8 +163,6 @@ A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8132AB7B430000C4C2F /* EEGReading.swift */; }; A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; A926D82B2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; - A92DD4D92B02D94F0062781B /* Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92DD4D82B02D94F0062781B /* Biopot.swift */; }; - A92DD4DD2B02DA3F0062781B /* Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92DD4D82B02D94F0062781B /* Biopot.swift */; }; A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */; }; A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34F12ADB9B9000FE0B51 /* OrderedCollections */; }; A9405B562A36856300C75412 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; @@ -215,8 +213,6 @@ A989112D2A36687B00E66E3A /* PatientListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112C2A36687B00E66E3A /* PatientListSheet.swift */; }; A989112F2A36688A00E66E3A /* PatientRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112E2A36688A00E66E3A /* PatientRow.swift */; }; A98911322A36689D00E66E3A /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; - A9A179532AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */; }; - A9A179542AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */; }; A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; A9A179572AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; @@ -247,6 +243,28 @@ A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */; }; A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; + A9D4B8D52B685D800054E27C /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */; }; + A9D4B8D62B685D800054E27C /* MuseHeadbandFitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */; }; + A9D4B8D72B685D800054E27C /* MuseBatteryDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */; }; + A9D4B8D82B685D800054E27C /* MuseAboutDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */; }; + A9D4B8D92B685D800054E27C /* MuseInterventionRequiredHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */; }; + A9D4B8DA2B685D800054E27C /* MuseConnectingProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */; }; + A9D4B8DB2B685D800054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */; }; + A9D4B8DC2B685D800054E27C /* MuseBatteryProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */; }; + A9D4B8DD2B685DEB0054E27C /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */; }; + A9D4B8DE2B685DEE0054E27C /* MuseHeadbandFitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */; }; + A9D4B8DF2B685DF00054E27C /* MuseBatteryDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */; }; + A9D4B8E02B685DF20054E27C /* MuseAboutDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */; }; + A9D4B8E12B685DF30054E27C /* MuseInterventionRequiredHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */; }; + A9D4B8E22B685DF60054E27C /* MuseConnectingProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */; }; + A9D4B8E32B685DF80054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */; }; + A9D4B8E42B685DFA0054E27C /* MuseBatteryProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */; }; + A9D4B8E62B6860AC0054E27C /* MuseHeadbandFitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */; }; + A9D4B8E72B68636A0054E27C /* MuseHeadbandFitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */; }; + A9D4B8E92B6863D60054E27C /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; + A9D4B8EA2B6863D60054E27C /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; + A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; + A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D83F922B081A47000D0C78 /* BiopotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D83F912B081A47000D0C78 /* BiopotTests.swift */; }; A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; @@ -291,10 +309,10 @@ 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BiopotDeviceDetailsView.swift; path = Views/BiopotDeviceDetailsView.swift; sourceTree = ""; }; 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGRecordings.swift; sourceTree = ""; }; 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCoordinator.swift; sourceTree = ""; }; - 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseTroublesConnectingHint.swift; sourceTree = ""; }; 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FinishedSetup.swift; sourceTree = ""; }; 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewPatientModel.swift; sourceTree = ""; }; 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGFrequency.swift; sourceTree = ""; }; + 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Questionnaire+Identifiable.swift"; sourceTree = ""; }; 2DC174FE89AC206431F90166 /* PatientInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientInformation.swift; sourceTree = ""; }; 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HeadbandFit+Muse.swift"; sourceTree = ""; }; 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMusePreset+Description.swift"; sourceTree = ""; }; @@ -361,7 +379,6 @@ A926D8122AB7B430000C4C2F /* EEGChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChannel.swift; sourceTree = ""; }; A926D8132AB7B430000C4C2F /* EEGReading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGReading.swift; sourceTree = ""; }; A926D8142AB7B430000C4C2F /* EEGChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChart.swift; sourceTree = ""; }; - A92DD4D82B02D94F0062781B /* Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Biopot.swift; sourceTree = ""; }; A9405B552A36856300C75412 /* AddPatientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPatientView.swift; sourceTree = ""; }; A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTile.swift; sourceTree = ""; }; A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTask.swift; sourceTree = ""; }; @@ -388,7 +405,6 @@ A98911312A36689D00E66E3A /* Patient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patient.swift; sourceTree = ""; }; A99522432AA61DA6009272F4 /* Muse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Muse.framework; sourceTree = ""; }; A99522462AA61FE5009272F4 /* NAMS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NAMS-Bridging-Header.h"; sourceTree = ""; }; - A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuseDeviceDetailsView.swift; sourceTree = ""; }; A9A179552AC62BE500B180D8 /* ListRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientSearchModel.swift; sourceTree = ""; }; @@ -403,6 +419,17 @@ A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceRow.swift; sourceTree = ""; }; A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGDeviceTests.swift; sourceTree = ""; }; A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; + A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceDetailsView.swift; sourceTree = ""; }; + A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseHeadbandFitSection.swift; sourceTree = ""; }; + A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseBatteryDetailsSection.swift; sourceTree = ""; }; + A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseAboutDetailsSection.swift; sourceTree = ""; }; + A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseInterventionRequiredHint.swift; sourceTree = ""; }; + A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseConnectingProblemsHint.swift; sourceTree = ""; }; + A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseHeadbandFitProblemsHint.swift; sourceTree = ""; }; + A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseBatteryProblemsHint.swift; sourceTree = ""; }; + A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuseHeadbandFitView.swift; sourceTree = ""; }; + A9D4B8E82B6863D60054E27C /* FitLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitLabel.swift; sourceTree = ""; }; + A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTileHeader.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPatientCard.swift; sourceTree = ""; }; A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTile.swift; sourceTree = ""; }; @@ -696,6 +723,7 @@ A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */, A945340B2AEAE6380095AAD3 /* TilesView.swift */, 2DC177BFA6C401C2C87FCD5C /* TileType.swift */, + A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */, ); path = Tiles; sourceTree = ""; @@ -737,17 +765,16 @@ path = Recording; sourceTree = ""; }; - A988FEAE2B04529B00022A61 /* BioPot */ = { + A988FEAE2B04529B00022A61 /* Biopot */ = { isa = PBXGroup; children = ( A97E4F212B1EA21000E25505 /* Recording */, A988FEB02B0452AB00022A61 /* Characteristics */, - A92DD4D82B02D94F0062781B /* Biopot.swift */, A988FEA92B0414FD00022A61 /* BiopotDevice.swift */, 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */, 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */, ); - path = BioPot; + path = Biopot; sourceTree = ""; }; A988FEB02B0452AB00022A61 /* Characteristics */ = { @@ -817,6 +844,7 @@ A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */, 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */, A9A179552AC62BE500B180D8 /* ListRow.swift */, + 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */, ); path = Utils; sourceTree = ""; @@ -835,7 +863,7 @@ isa = PBXGroup; children = ( A9C82FB52B632EC3004703E0 /* MaybeExternal */, - A988FEAE2B04529B00022A61 /* BioPot */, + A988FEAE2B04529B00022A61 /* Biopot */, A926D8342AB7C2CC000C4C2F /* Mock */, A99522402AA61D82009272F4 /* Muse */, A988FEAB2B043AED00022A61 /* BatteryIcon.swift */, @@ -845,6 +873,30 @@ path = Devices; sourceTree = ""; }; + A9D4B8CB2B685D800054E27C /* Details */ = { + isa = PBXGroup; + children = ( + A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */, + A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */, + A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */, + A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */, + A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */, + A9D4B8E82B6863D60054E27C /* FitLabel.swift */, + ); + path = Details; + sourceTree = ""; + }; + A9D4B8D02B685D800054E27C /* Hints */ = { + isa = PBXGroup; + children = ( + A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */, + A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */, + A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */, + A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */, + ); + path = Hints; + sourceTree = ""; + }; A9DF79E82AE9B97C00AB5983 /* Testing */ = { isa = PBXGroup; children = ( @@ -857,10 +909,10 @@ A9EB34D02B64DAB000FD62C3 /* Views */ = { isa = PBXGroup; children = ( - A9A179522AC6266A00B180D8 /* MuseDeviceDetailsView.swift */, + A9D4B8CB2B685D800054E27C /* Details */, + A9D4B8D02B685D800054E27C /* Hints */, 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */, 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */, - 2DC17232D9209D6949DED2C3 /* MuseTroublesConnectingHint.swift */, ); path = Views; sourceTree = ""; @@ -1138,10 +1190,10 @@ A926D8222AB7B430000C4C2F /* EEGRecording.swift in Sources */, A916ADD52AB60227006960DF /* NotificationPermissions.swift in Sources */, A907DA302B192FD500FB69FB /* DataControl.swift in Sources */, - A9A179532AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */, A94533FA2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */, A9BCB58C2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, + A9D4B8D92B685D800054E27C /* MuseInterventionRequiredHint.swift in Sources */, A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */, 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */, A916ADD72AB62012006960DF /* ProcessInfo+PreviewSimulator.swift in Sources */, @@ -1155,15 +1207,18 @@ A989112D2A36687B00E66E3A /* PatientListSheet.swift in Sources */, 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, A94A42B22AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, - A92DD4D92B02D94F0062781B /* Biopot.swift in Sources */, + A9D4B8D72B685D800054E27C /* MuseBatteryDetailsSection.swift in Sources */, A926D8242AB7B430000C4C2F /* EEGSeries.swift in Sources */, A97E4F232B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */, + A9D4B8D62B685D800054E27C /* MuseHeadbandFitSection.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, + A9D4B8DC2B685D800054E27C /* MuseBatteryProblemsHint.swift in Sources */, A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */, A907DA362B1942B800FB69FB /* DataAcquisition.swift in Sources */, + A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, A988FEB52B0453E100022A61 /* DeviceInformation.swift in Sources */, A9F2ECD02AEC5EF50057C7DD /* MeasurementTask.swift in Sources */, @@ -1191,6 +1246,7 @@ A967061C2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */, 2DC174CC46386A3F7E20786B /* IXNMuseVersion+String.swift in Sources */, 2DC179BD6217C86A55D70E6F /* IXNMuseConfiguration+Description.swift in Sources */, + A9D4B8DA2B685D800054E27C /* MuseConnectingProblemsHint.swift in Sources */, A988FEB22B0452C400022A61 /* DeviceConfiguration.swift in Sources */, 2DC17FE0AC1DD98C29B417F1 /* IXNMusePreset+Description.swift in Sources */, A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */, @@ -1205,6 +1261,7 @@ A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */, A907DA3F2B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, + A9D4B8E62B6860AC0054E27C /* MuseHeadbandFitView.swift in Sources */, 2DC17F8981E53AEED0CFD1BD /* MockDevice.swift in Sources */, 2DC17C29AA0382E9F5F2AA4D /* MockMeasurementGenerator.swift in Sources */, 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */, @@ -1212,12 +1269,13 @@ A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */, A9C82FB92B633906004703E0 /* LoadingSectionHeader.swift in Sources */, + A9D4B8D52B685D800054E27C /* MuseDeviceDetailsView.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */, 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, 2DC17257A28A7E2232229658 /* PatientTask.swift in Sources */, 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */, 2DC1776253EA8999F231D6DA /* MuseDevice.swift in Sources */, - 2DC17E464BAEACF3A2B554B5 /* MuseTroublesConnectingHint.swift in Sources */, + A9D4B8D82B685D800054E27C /* MuseAboutDetailsSection.swift in Sources */, 2DC17C61E1697767983916EF /* DeviceCoordinator.swift in Sources */, 2DC17735CC338B30ECB656B4 /* MuseDeviceRow.swift in Sources */, 2DC17CAFDF6974EAA57C5D34 /* MuseDeviceList.swift in Sources */, @@ -1227,7 +1285,10 @@ 2DC17100BA13C8B5325EBD94 /* EEGRecordings.swift in Sources */, 2DC17A3ADA039E3FD901D8CF /* HeadbandFit.swift in Sources */, 2DC17D19CFECCEF0A7206F35 /* ConnectionState.swift in Sources */, + A9D4B8E92B6863D60054E27C /* FitLabel.swift in Sources */, 2DC171EFD6619742C29254CE /* Fit.swift in Sources */, + 2DC17E6D396A8A2B50E1D44F /* Questionnaire+Identifiable.swift in Sources */, + A9D4B8DB2B685D800054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1273,12 +1334,13 @@ A907DA312B192FD500FB69FB /* DataControl.swift in Sources */, A9DF79E02AE8A82100AB5983 /* PatientListSheet.swift in Sources */, A94533FB2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */, + A9D4B8E22B685DF60054E27C /* MuseConnectingProblemsHint.swift in Sources */, A926D7822AB7A552000C4C2F /* NotificationPermissions.swift in Sources */, - A9A179542AC6266A00B180D8 /* MuseDeviceDetailsView.swift in Sources */, A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */, A926D7872AB7A552000C4C2F /* Welcome.swift in Sources */, A926D7882AB7A552000C4C2F /* Binding+Negate.swift in Sources */, A94A42B72AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, + A9D4B8DD2B685DEB0054E27C /* MuseDeviceDetailsView.swift in Sources */, A926D78A2AB7A552000C4C2F /* ProcessInfo+PreviewSimulator.swift in Sources */, A9F2ECC72AEB27B10057C7DD /* MeasurementTile.swift in Sources */, A9DF79E32AE8A82B00AB5983 /* SelectedPatientCard.swift in Sources */, @@ -1287,9 +1349,9 @@ A926D78B2AB7A552000C4C2F /* OnboardingFlow+PreviewSimulator.swift in Sources */, A926D78C2AB7A552000C4C2F /* Home.swift in Sources */, A94A42B32AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, - A92DD4DD2B02DA3F0062781B /* Biopot.swift in Sources */, A926D8252AB7B430000C4C2F /* EEGSeries.swift in Sources */, A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */, + A9D4B8E42B685DFA0054E27C /* MuseBatteryProblemsHint.swift in Sources */, A97E4F242B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */, A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, A926D78E2AB7A552000C4C2F /* OnboardingFlow.swift in Sources */, @@ -1297,6 +1359,7 @@ A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */, A907DA372B1942B800FB69FB /* DataAcquisition.swift in Sources */, A926D7912AB7A552000C4C2F /* CodableArray+RawRepresentable.swift in Sources */, + A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */, A926D7922AB7A552000C4C2F /* FeatureFlags.swift in Sources */, A988FEB62B0453E100022A61 /* DeviceInformation.swift in Sources */, A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */, @@ -1313,6 +1376,7 @@ A9DF79E22AE8A82600AB5983 /* PatientInformation.swift in Sources */, A94534112AEAF2AE0095AAD3 /* CompletedTask.swift in Sources */, A926D79C2AB7A552000C4C2F /* NAMSApp.swift in Sources */, + A9D4B8DF2B685DF00054E27C /* MuseBatteryDetailsSection.swift in Sources */, A988FEAF2B0452A900022A61 /* BiopotDevice.swift in Sources */, A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */, A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */, @@ -1320,7 +1384,9 @@ A926D8042AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */, A988FEAD2B043AED00022A61 /* BatteryIcon.swift in Sources */, + A9D4B8E02B685DF20054E27C /* MuseAboutDetailsSection.swift in Sources */, 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */, + A9D4B8DE2B685DEE0054E27C /* MuseHeadbandFitSection.swift in Sources */, A967061D2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */, 2DC17374D5266F13ADD5C002 /* MockDeviceManager.swift in Sources */, 2DC179831EDABDCBD8A1EBFB /* IXNMuseVersion+String.swift in Sources */, @@ -1336,10 +1402,12 @@ A9F2ECCE2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */, A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */, + A9D4B8E72B68636A0054E27C /* MuseHeadbandFitView.swift in Sources */, 2DC17FB80AB25F5356A59FEE /* HeadbandFit+Muse.swift in Sources */, A907DA402B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, A9DF79E12AE8A82300AB5983 /* PatientRow.swift in Sources */, + A9D4B8E12B685DF30054E27C /* MuseInterventionRequiredHint.swift in Sources */, 2DC176E7B29173393F43A357 /* MockDevice.swift in Sources */, A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, 2DC179F4A6B69C07C1A440D2 /* MockMeasurementGenerator.swift in Sources */, @@ -1349,7 +1417,7 @@ 2DC1727A98890570E5A4B46D /* PatientTask.swift in Sources */, 2DC17206C9867ACAC0915363 /* BiopotDevicePreview.swift in Sources */, 2DC17F50A02902B6757A435B /* MuseDevice.swift in Sources */, - 2DC17F033D0282AEE8945A47 /* MuseTroublesConnectingHint.swift in Sources */, + A9D4B8E32B685DF80054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */, 2DC17877F0B86FDEC613124B /* DeviceCoordinator.swift in Sources */, 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */, 2DC17A843968AEFAB1B64C3B /* MuseDeviceList.swift in Sources */, @@ -1358,8 +1426,10 @@ 2DC17B4DB25C9775EF301CD0 /* BiopotDeviceDetailsView.swift in Sources */, 2DC17995C927FACA6C0146B3 /* EEGRecordings.swift in Sources */, 2DC1776FFC7360126770D201 /* HeadbandFit.swift in Sources */, + A9D4B8EA2B6863D60054E27C /* FitLabel.swift in Sources */, 2DC1707B04BFF1D66B3F913D /* ConnectionState.swift in Sources */, 2DC17BAFF043EBD5240BDD76 /* Fit.swift in Sources */, + 2DC172F25F327CB4B718D589 /* Questionnaire+Identifiable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/NAMS/Devices/BioPot/Biopot.swift b/NAMS/Devices/BioPot/Biopot.swift deleted file mode 100644 index 67d5e82..0000000 --- a/NAMS/Devices/BioPot/Biopot.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Spezi -import SpeziBluetooth -import SpeziViews -import SwiftUI - - -struct Biopot: View { - @Environment(Bluetooth.self) - private var bluetooth - @Environment(BiopotDevice.self) - private var biopot: BiopotDevice? - - @State private var viewState: ViewState = .idle - - var body: some View { - let devices = bluetooth.nearbyDevices(for: BiopotDevice.self) - - // TODO: We need some place to put our modifiers! - Section { - Text("Make sure your device is connected and nearby!") - .listRowBackground(Color.clear) - .listRowInsets(.init(top: 0, leading: 0.2, bottom: 0, trailing: 0.2)) - .viewStateAlert(state: $viewState) - .scanNearbyDevices(with: bluetooth, autoConnect: true) - } - - if devices.isEmpty { - VStack { // TODO: Reuse! - Text("Searching for nearby devices ...") - .foregroundColor(.secondary) - ProgressView() - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - } else { - Section { - ForEach(devices) { device in - if let name = device.name { - ListRow(name) { - Text(device.state.localizedStringResource) - } - } - } - } header: { - HStack { // TODO: reuse! - Text("Devices") - .padding(.trailing, 10) - if bluetooth.isScanning { - ProgressView() - } - } - } - } - - testingSupport - - if let biopot, - let info = biopot.service.deviceInfo { - Section("Status") { - ListRow("BATTERY") { - BatteryIcon(percentage: Int(info.batteryLevel)) - } - ListRow("Charging") { - if info.batteryCharging { - Text("Yes") - } else { - Text("No") - } - } - ListRow("Temperature") { - Text("\(info.temperatureValue) °C") - } - if let serialNumber = biopot.deviceInformation.serialNumber { - ListRow("Serial Number") { - Text(serialNumber) - } - } - if let firmwareVersion = biopot.deviceInformation.firmwareRevision { - ListRow("Firmware Version") { - Text(firmwareVersion) - } - } - if let hardwareVersion = biopot.deviceInformation.hardwareRevision { - ListRow("Hardware Version") { - Text(hardwareVersion) - } - } - } - - actionButtons - } else if biopot != nil { - Section { - ProgressView() - .listRowBackground(Color.clear) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .frame(maxWidth: .infinity) - } - } - } - - @MainActor @ViewBuilder private var testingSupport: some View { - if FeatureFlags.testBiopot { - Button("Receive Device Info") { - // TODO: allow testing support via a different SPI? - // TODO: also this needs to inject a device instance? - /* - biopot.deviceInfo = DeviceInformation( - syncRatio: 0, - syncMode: false, - memoryWriteNumber: 0, - memoryEraseMode: false, - batteryLevel: 80, - temperatureValue: 23, - batteryCharging: false - )*/ - } - } - } - - @MainActor @ViewBuilder private var actionButtons: some View { - Section("Actions") { // section of testing actions - AsyncButton("Query Device Information", state: $viewState) { - try await biopot?.deviceInformation.retrieveDeviceInformation() - } - AsyncButton("Read Device Configuration", state: $viewState) { - try await biopot?.service.$deviceInfo.read() - } - AsyncButton("Read Data Control", state: $viewState) { - try await biopot?.service.$dataControl.read() - } - AsyncButton("Read Data Acquisition", state: $viewState) { - try await biopot?.service.$impedanceMeasurement.read() - } - AsyncButton("Read Sample Configuration", state: $viewState) { - try await biopot?.service.$samplingConfiguration.read() - } - } - } -} - - -extension BluetoothState: CustomLocalizedStringResourceConvertible { - public var localizedStringResource: LocalizedStringResource { - switch self { - case .unknown: - "Unknown" - case .poweredOn: - "Bluetooth On" - case .unsupported: - "Bluetooth Unsupported" - case .poweredOff: - "Bluetooth Off" - case .unauthorized: - "Bluetooth Unauthorized" - } - } -} - -extension PeripheralState: CustomLocalizedStringResourceConvertible { - public var localizedStringResource: LocalizedStringResource { - switch self { - case .connected: - "Connected" - case .disconnected: - "Disconnected" - case .connecting: - "Connecting" - case .disconnecting: - "Disconnecting" - } - } -} - - -#if DEBUG -#Preview { - List { - Biopot() - } - .previewWith { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } - } -} -#endif diff --git a/NAMS/Devices/BioPot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift similarity index 95% rename from NAMS/Devices/BioPot/BiopotDevice.swift rename to NAMS/Devices/Biopot/BiopotDevice.swift index ffca37b..f4222d7 100644 --- a/NAMS/Devices/BioPot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -46,7 +46,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { @DeviceState(\.id) var id @DeviceState(\.state) - var state + var state // TODO: state update doesn't really work well (sometimes stale!) @DeviceState(\.name) var name @@ -62,7 +62,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { @Service(id: .deviceInformationService) var deviceInformation = DeviceInformationService() // TODO: make sure we read once we are connected! - @Service(id: .biopotService) + @Service(id: .biopotService) // TODO: have the service self-contained by having the id in there, allows type based discovery criteria var service = BiopotService() @MainActor private var recordingSession: EEGRecordingSession? @@ -95,9 +95,10 @@ class BiopotDevice: BluetoothDevice, Identifiable { } @MainActor - func stopRecording() { + func stopRecording() async throws { + try await service.$dataControl.write(false) + startDate = nil recordingSession = nil - // TODO: async operation to stop data collection } @MainActor diff --git a/NAMS/Devices/BioPot/Characteristics/AccelerometerSample.swift b/NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/AccelerometerSample.swift rename to NAMS/Devices/Biopot/Characteristics/AccelerometerSample.swift diff --git a/NAMS/Devices/BioPot/Characteristics/ByteBuffer+Int24.swift b/NAMS/Devices/Biopot/Characteristics/ByteBuffer+Int24.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/ByteBuffer+Int24.swift rename to NAMS/Devices/Biopot/Characteristics/ByteBuffer+Int24.swift diff --git a/NAMS/Devices/BioPot/Characteristics/DataAcquisition.swift b/NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/DataAcquisition.swift rename to NAMS/Devices/Biopot/Characteristics/DataAcquisition.swift diff --git a/NAMS/Devices/BioPot/Characteristics/DataControl.swift b/NAMS/Devices/Biopot/Characteristics/DataControl.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/DataControl.swift rename to NAMS/Devices/Biopot/Characteristics/DataControl.swift diff --git a/NAMS/Devices/BioPot/Characteristics/DeviceConfiguration.swift b/NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/DeviceConfiguration.swift rename to NAMS/Devices/Biopot/Characteristics/DeviceConfiguration.swift diff --git a/NAMS/Devices/BioPot/Characteristics/DeviceInformation.swift b/NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/DeviceInformation.swift rename to NAMS/Devices/Biopot/Characteristics/DeviceInformation.swift diff --git a/NAMS/Devices/BioPot/Characteristics/EEGSample.swift b/NAMS/Devices/Biopot/Characteristics/EEGSample.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/EEGSample.swift rename to NAMS/Devices/Biopot/Characteristics/EEGSample.swift diff --git a/NAMS/Devices/BioPot/Characteristics/ImpedanceMeasurement.swift b/NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/ImpedanceMeasurement.swift rename to NAMS/Devices/Biopot/Characteristics/ImpedanceMeasurement.swift diff --git a/NAMS/Devices/BioPot/Characteristics/SamplingConfiguration.swift b/NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift similarity index 100% rename from NAMS/Devices/BioPot/Characteristics/SamplingConfiguration.swift rename to NAMS/Devices/Biopot/Characteristics/SamplingConfiguration.swift diff --git a/NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift b/NAMS/Devices/Biopot/Recording/EEGChannel+Biopot.swift similarity index 100% rename from NAMS/Devices/BioPot/Recording/EEGChannel+Biopot.swift rename to NAMS/Devices/Biopot/Recording/EEGChannel+Biopot.swift diff --git a/NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift similarity index 78% rename from NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift rename to NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift index 57a992e..b80b2c1 100644 --- a/NAMS/Devices/BioPot/Views/BiopotDeviceDetailsView.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift @@ -28,7 +28,7 @@ struct BiopotDeviceDetailsView: View { Section("About") { if let firmware = biopot.deviceInformation.firmwareRevision { - ListRow("FIRMWARE_VERSION") { + ListRow("Firmware Version") { Text(verbatim: firmware) } } @@ -38,7 +38,7 @@ struct BiopotDeviceDetailsView: View { } } if let serialNumber = biopot.deviceInformation.serialNumber { - ListRow("SERIAL_NUMBER") { + ListRow("Serial Number") { Text(verbatim: serialNumber) } } @@ -48,13 +48,18 @@ struct BiopotDeviceDetailsView: View { disconnectClosure() dismiss() }) { - Text("DISCONNECT") + Text("Disconnect") .frame(maxWidth: .infinity) } - // TODO: .disabled(!state.associatedConnection) + .disabled(biopot.state == .disconnected || biopot.state == .disconnecting) } - .navigationTitle(Text(verbatim: biopot.name!)) // TODO: avoid! .navigationBarTitleDisplayMode(.inline) + .navigationTitle(Text(verbatim: biopot.name ?? "")) + .onChange(of: biopot.state) { + if biopot.state == .disconnected || biopot.state == .disconnecting { + dismiss() + } + } } diff --git a/NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift similarity index 85% rename from NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift rename to NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index 90cbda6..b1172d6 100644 --- a/NAMS/Devices/BioPot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -19,15 +19,13 @@ struct BiopotDeviceRow: View { var body: some View { + // TODO: how to UI test the biopot? NearbyDeviceRow(peripheral: device) { Task { await deviceCoordinator.tapDevice(.biopot(device)) } } secondaryAction: { - // TODO: we assume I button only shows if this is true! - if device.state == .connected { - presentingActiveDevice = device - } + presentingActiveDevice = device } .navigationDestination(item: $presentingActiveDevice) { device in BiopotDeviceDetailsView(device: device) { diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index 660e2ea..e9da082 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -59,16 +59,16 @@ enum SomeDevice { // TODO: move to separate file } @MainActor - func stopRecording() { + func stopRecording() async throws { switch self { #if MUSE case let .muse(muse): - muse.stopRecording() + try await muse.stopRecording() #endif case let .biopot(biopot): - biopot.stopRecording() + try await biopot.stopRecording() case let .mock(mock): - mock.stopRecording() + try await mock.stopRecording() } } } diff --git a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift index f9b2f59..ab0457b 100644 --- a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift +++ b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift @@ -17,7 +17,7 @@ public protocol GenericBluetoothPeripheral { var state: PeripheralState { get } - var requiresUserAttention: Bool { get } // TODO: optional? + var requiresUserAttention: Bool { get } } @@ -81,7 +81,7 @@ public struct NearbyDeviceRow: View { } } } - .frame(maxWidth: .infinity) // required for UI tests // TODO: does this break stuff? + // .frame(maxWidth: .infinity) // required for UI tests // TODO: does this break stuff? yes breaks visuals? if secondaryActionClosure != nil, case .connected = peripheral.state { Button("DEVICE_DETAILS", systemImage: "info.circle", action: deviceDetailsAction) @@ -110,7 +110,6 @@ public struct NearbyDeviceRow: View { @ViewBuilder var accessibilityRepresentation: some View { let button = Button(action: devicePrimaryAction) { - // TODO: how to provide a different primary label (for Muse?)? Text(verbatim: peripheral.accessibilityLabel) if let localizationSecondaryLabel { Text(localizationSecondaryLabel) @@ -169,16 +168,20 @@ public struct NearbyDeviceRow: View { List { NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 1", state: .connecting)) { print("Clicked") - } secondaryAction: {} + } secondaryAction: { + } NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 2", state: .connected)) { print("Clicked") - } secondaryAction: {} + } secondaryAction: { + } NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "Long MyDevice 3", state: .connected, requiresUserAttention: true)) { print("Clicked") - } secondaryAction: {} + } secondaryAction: { + } NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 4", state: .disconnecting)) { print("Clicked") - } secondaryAction: {} + } secondaryAction: { + } } } #endif diff --git a/NAMS/Devices/Mock/MockDevice.swift b/NAMS/Devices/Mock/MockDevice.swift index c543606..df4966b 100644 --- a/NAMS/Devices/Mock/MockDevice.swift +++ b/NAMS/Devices/Mock/MockDevice.swift @@ -85,7 +85,12 @@ class MockDevice { } private func handleConnected() { - self.deviceInformation = MuseDeviceInformation(serialNumber: "0xAABBCCDD", firmwareVersion: "1.2.0", remainingBatteryPercentage: 75) + self.deviceInformation = MuseDeviceInformation( + serialNumber: "0xAABBCCDD", + firmwareVersion: "1.2", + hardwareVersion: "1.0", + remainingBatteryPercentage: 75 + ) task = Task { @MainActor [weak self] in try? await Task.sleep(for: .seconds(3)) @@ -120,7 +125,7 @@ class MockDevice { } @MainActor - func stopRecording() { + func stopRecording() async throws { self.eegTimer = nil self.recordingSession = nil } diff --git a/NAMS/Devices/Mock/MockDeviceManager.swift b/NAMS/Devices/Mock/MockDeviceManager.swift index dfcb07f..b1043b8 100644 --- a/NAMS/Devices/Mock/MockDeviceManager.swift +++ b/NAMS/Devices/Mock/MockDeviceManager.swift @@ -21,7 +21,7 @@ class MockDeviceManager { private let storedDevicesList: [MockDevice] - // TODO: isScanning property? + @ObservationIgnored private var previouslyDiscovered = false var nearbyDevices: [MockDevice] = [] @ObservationIgnored private var task: Task? { willSet { @@ -39,12 +39,16 @@ class MockDeviceManager { func startScanning() { - task = Task { @MainActor in - // TODO: instant discovery of previously discovered devices - try? await Task.sleep(for: .seconds(2)) - guard !Task.isCancelled else { - return + if !previouslyDiscovered { + task = Task { @MainActor in + try? await Task.sleep(for: .seconds(2)) + guard !Task.isCancelled else { + return + } + nearbyDevices = storedDevicesList + previouslyDiscovered = true } + } else { nearbyDevices = storedDevicesList } } diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift index 549d3a4..1ece8f4 100644 --- a/NAMS/Devices/Mock/Views/MockDeviceRow.swift +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -24,17 +24,13 @@ struct MockDeviceRow: View { await deviceCoordinator.tapDevice(.mock(device)) } } secondaryAction: { - if device.state == .connected { - presentingActiveDevice = device - } + presentingActiveDevice = device } .navigationDestination(item: $presentingActiveDevice) { device in - if let info = device.deviceInformation { - MuseDeviceDetailsView(model: device.label, state: device.connectionState, info) { - device.disconnect() - // TODO: this needs a better approach - deviceCoordinator.hintDisconnect() - } + MuseDeviceDetailsView(model: device.label, state: device.connectionState, device.deviceInformation) { + device.disconnect() + // TODO: this needs a better approach + deviceCoordinator.hintDisconnect() } } } @@ -47,5 +43,15 @@ struct MockDeviceRow: View { #if DEBUG -// TODO: preview +#Preview { + NavigationStack { + List { + MockDeviceRow(device: MockDevice(name: "Device 1")) + MockDeviceRow(device: MockDevice(name: "Device 2", state: .connected)) + } + .previewWith { + DeviceCoordinator() + } + } +} #endif diff --git a/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift b/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift index eb7e8df..467c378 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMuseConfiguration+Description.swift @@ -13,25 +13,25 @@ import Foundation extension IXNMuseConfiguration { var configurationString: String { """ - model: \(getModel()) \ - serialNumber: \(getSerialNumber()) \ + model: \(getModel()), \ + serialNumber: \(getSerialNumber()), \ headbandName: \(getHeadbandName()), \ - bluetoothMac: \(getBluetoothMac()) \ - batteryDataEnabled: \(getBatteryDataEnabled()) \ - batteryPercentRemaining: \(getBatteryPercentRemaining()) \ + bluetoothMac: \(getBluetoothMac()), \ + batteryDataEnabled: \(getBatteryDataEnabled()), \ + batteryPercentRemaining: \(getBatteryPercentRemaining()), \ preset: \(getPreset().description), \ - microcontrollerId: \(getMicrocontrollerId()) \ - eegChannelCount: \(getEegChannelCount()) \ - afeGain: \(getAfeGain()) \ - downsampleRate: \(getDownsampleRate()) \ - seroutMode: \(getSeroutMode()) \ - outputFrequency: \(getOutputFrequency()) \ - adcFrequency: \(getAdcFrequency()) \ - notchFilterEnabled: \(getNotchFilterEnabled()) \ - notchFilter: \(getNotchFilter().description) \ - accelerometerSampleFrequency: \(getAccelerometerSampleFrequency()) \ - drlRefEnabled: \(getDrlRefEnabled()) \ - drlRefFrequency: \(getDrlRefFrequency()) \ + microcontrollerId: \(getMicrocontrollerId()), \ + eegChannelCount: \(getEegChannelCount()), \ + afeGain: \(getAfeGain()), \ + downsampleRate: \(getDownsampleRate()), \ + seroutMode: \(getSeroutMode()), \ + outputFrequency: \(getOutputFrequency()), \ + adcFrequency: \(getAdcFrequency()), \ + notchFilterEnabled: \(getNotchFilterEnabled()), \ + notchFilter: \(getNotchFilter().description), \ + accelerometerSampleFrequency: \(getAccelerometerSampleFrequency()), \ + drlRefEnabled: \(getDrlRefEnabled()), \ + drlRefFrequency: \(getDrlRefFrequency()), \ licensingNonce: \(getLicenseNonce()) """ } diff --git a/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift b/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift index b8fdfcf..d345d6b 100644 --- a/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift +++ b/NAMS/Devices/Muse/Extensions/IXNMuseVersion+String.swift @@ -14,8 +14,8 @@ extension IXNMuseVersion { firmware: \(getFirmwareVersion()) (\(getFirmwareBuildNumber()), \(getFirmwareType())), \ hardware: \(getHardwareVersion()), \ protocol: \(getProtocolVersion()), \ - bsp: \(getBspVersion()) \ - bootloaderVersion: \(getBootloaderVersion()) \ + bsp: \(getBspVersion()), \ + bootloaderVersion: \(getBootloaderVersion()), \ runningState: \(getRunningState()) """ } diff --git a/NAMS/Devices/Muse/Model/ConnectionState.swift b/NAMS/Devices/Muse/Model/ConnectionState.swift index d0a0c17..c0ecebd 100644 --- a/NAMS/Devices/Muse/Model/ConnectionState.swift +++ b/NAMS/Devices/Muse/Model/ConnectionState.swift @@ -9,8 +9,6 @@ import Foundation -// TODO: move some of these files to the Muse folder? - enum ConnectionState { case unknown case connected @@ -42,23 +40,6 @@ enum ConnectionState { extension ConnectionState: Equatable {} -// TODO: are any of these still used? -extension ConnectionState: CustomLocalizedStringResourceConvertible { - public var localizedStringResource: LocalizedStringResource { - switch self { - case .disconnected, .unknown: - return "DISCONNECTED" - case .connecting: - return "CONNECTING" - case .connected: - return "CONNECTED" - case .interventionRequired: - return "INTERVENTION_REQUIRED" - } - } -} - - extension ConnectionState: CustomStringConvertible { public var description: String { switch self { diff --git a/NAMS/Devices/Muse/Model/Fit.swift b/NAMS/Devices/Muse/Model/Fit.swift index dea2cdc..de3970e 100644 --- a/NAMS/Devices/Muse/Model/Fit.swift +++ b/NAMS/Devices/Muse/Model/Fit.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import SwiftUI +import Foundation enum Fit: String, Hashable, CustomLocalizedStringResourceConvertible { @@ -18,22 +18,11 @@ enum Fit: String, Hashable, CustomLocalizedStringResourceConvertible { var localizedStringResource: LocalizedStringResource { switch self { case .good: - return "GOOD_FIT" + .init("good", comment: "Muse headband fit") case .mediocre: - return "MEDIOCRE_FIT" + .init("mediocre", comment: "Muse headband fit") case .poor: - return "POOR_FIT" - } - } - - var style: Color { - switch self { - case .good: - return .green - case .mediocre: - return .orange - case .poor: - return .red + .init("poor", comment: "Muse headband fit") } } } diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift index cea50a9..f846748 100644 --- a/NAMS/Devices/Muse/MuseDevice.swift +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -15,6 +15,7 @@ import SpeziBluetooth class MuseDeviceInformation { let serialNumber: String let firmwareVersion: String + let hardwareVersion: String /// Remaining battery percentage in percent [0.0;100.0] var remainingBatteryPercentage: Double? @@ -26,20 +27,20 @@ class MuseDeviceInformation { /// Determines if the last second of data is considered good var isGood: (Bool, Bool, Bool, Bool) = (false, false, false, false) // swiftlint:disable:this large_tuple - // TODO: change above thingy! (similar to below!) - /// The current fit of the headband var fit: HeadbandFit? init( serialNumber: String, firmwareVersion: String, - remainingBatteryPercentage: Double?, + hardwareVersion: String, + remainingBatteryPercentage: Double? = nil, wearingHeadband: Bool = false, fit: HeadbandFit? = nil ) { self.serialNumber = serialNumber self.firmwareVersion = firmwareVersion + self.hardwareVersion = hardwareVersion self.remainingBatteryPercentage = remainingBatteryPercentage self.wearingHeadband = wearingHeadband self.fit = fit @@ -60,7 +61,7 @@ class MuseDevice: Identifiable { .alphaAbsolute, // 8-16 Hz .betaAbsolute, // 16-32 Hz .gammaAbsolute, // 32-64 Hz - // .eeg, // TODO: we are interested in querying ALL data! + // .eeg, // enables collection of raw data .hsiPrecision ] @@ -111,8 +112,7 @@ class MuseDevice: Identifiable { self.connectionState = ConnectionState(from: muse.getConnectionState()) self.connectionListener = ConnectionListener(device: self) - self.muse.setNumConnectTries(0) // TODO: does this interfere with anything? - // TODO: error listener? + self.muse.setNumConnectTries(0) } func connect() { @@ -138,11 +138,10 @@ class MuseDevice: Identifiable { @MainActor func startRecording(_ session: EEGRecordingSession) async throws { self.recordingSession = session - // TODO: only enable recording upon request? } @MainActor - func stopRecording() { + func stopRecording() async throws { self.recordingSession = nil } @@ -172,21 +171,26 @@ class MuseDevice: Identifiable { logger.debug("\(self.label): Connected. Versions: \(version.versionString); Configuration: \(configuration.configurationString)") - self.deviceInformation = MuseDeviceInformation( // TODO: other info that is relevant? + self.deviceInformation = MuseDeviceInformation( serialNumber: configuration.getSerialNumber(), firmwareVersion: version.getFirmwareVersion(), + hardwareVersion: version.getHardwareVersion(), remainingBatteryPercentage: configuration.getBatteryPercentRemaining() ) } @MainActor - private func receive(_ packet: IXNMuseDataPacket, muse: IXNMuse) { + private func receive(_ packet: IXNMuseDataPacket, muse: IXNMuse) { // swiftlint:disable:this cyclomatic_complexity + guard let deviceInformation else { + return + } + switch packet.packetType() { case .hsiPrecision: let fit = HeadbandFit(from: packet) - if deviceInformation?.fit != fit { // TODO: replace all the optional accesses (we have a class now) - deviceInformation?.fit = fit + if deviceInformation.fit != fit { + deviceInformation.fit = fit } case .eeg: recordingSession?.append(series: EEGSeries(from: packet), for: .all) @@ -199,9 +203,9 @@ class MuseDevice: Identifiable { case .gammaAbsolute: recordingSession?.append(series: EEGSeries(from: packet), for: .gamma) case .battery: - deviceInformation?.remainingBatteryPercentage = packet.getBatteryValue(.chargePercentageRemaining) + deviceInformation.remainingBatteryPercentage = packet.getBatteryValue(.chargePercentageRemaining) case .isGood: - deviceInformation?.isGood = ( + deviceInformation.isGood = ( packet.getEegChannelValue(.EEG1) == 1.0, packet.getEegChannelValue(.EEG2) == 1.0, packet.getEegChannelValue(.EEG3) == 1.0, diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 48e546f..1d7f7d3 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -31,7 +31,7 @@ class MuseDeviceManager { logger.debug("Initialized Muse Manager with API version \(apiVersion.getString())") } - self.museManager.removeFromList(after: 6) // stale timeout if there isn't an updated advertisement TODO: verify 5s? + self.museManager.removeFromList(after: 6) // stale timeout if there isn't an updated advertisement } func startScanning() { diff --git a/NAMS/Devices/Muse/Views/Details/FitLabel.swift b/NAMS/Devices/Muse/Views/Details/FitLabel.swift new file mode 100644 index 0000000..10f31c8 --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/FitLabel.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct FitLabel: View { + private let fit: Fit + + var body: some View { + Text(fit.localizedStringResource) + .foregroundStyle(fit.style) + } + + init(_ fit: Fit) { + self.fit = fit + } +} + + +extension Fit { + fileprivate var style: Color { + switch self { + case .good: + return .green + case .mediocre: + return .orange + case .poor: + return .red + } + } +} + + +#if DEBUG +#Preview { + List { + FitLabel(.good) + FitLabel(.mediocre) + FitLabel(.poor) + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift b/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift new file mode 100644 index 0000000..018524f --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift @@ -0,0 +1,45 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseAboutDetailsSection: View { + private let deviceInformation: MuseDeviceInformation + + + var body: some View { + Section("About") { + ListRow("Firmware Version") { + Text(verbatim: deviceInformation.firmwareVersion) + } + ListRow("Hardware Version") { + Text(verbatim: deviceInformation.hardwareVersion) + } + ListRow("Serial Number") { + Text(verbatim: deviceInformation.serialNumber) + } + } + } + + + init(_ deviceInformation: MuseDeviceInformation) { + self.deviceInformation = deviceInformation + } +} + + +#if DEBUG +#Preview { + List { + MuseAboutDetailsSection( + .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1") + ) + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift new file mode 100644 index 0000000..5aaf864 --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift @@ -0,0 +1,42 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseBatteryDetailsSection: View { + private let deviceInformation: MuseDeviceInformation + + var body: some View { + if let remainingBattery = deviceInformation.remainingBatteryPercentage { + Section { + ListRow("BATTERY") { + BatteryIcon(percentage: Int(remainingBattery)) + } + } footer: { + MuseBatteryProblemsHint() + } + } + } + + + init(_ deviceInformation: MuseDeviceInformation) { + self.deviceInformation = deviceInformation + } +} + + +#if DEBUG +#Preview { + List { + MuseBatteryDetailsSection( + .init(serialNumber: "", firmwareVersion: "", hardwareVersion: "", remainingBatteryPercentage: 75) + ) + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift new file mode 100644 index 0000000..ca5cff7 --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift @@ -0,0 +1,116 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseDeviceDetailsView: View { + private let model: String + private let state: ConnectionState + private let deviceInformation: MuseDeviceInformation? + private let disconnectClosure: () -> Void + + @Environment(\.dismiss) + private var dismiss + + var body: some View { + List { + if case let .interventionRequired(message) = state { + MuseInterventionRequiredHint(message) + } + + if let deviceInformation { + MuseBatteryDetailsSection(deviceInformation) + MuseHeadbandFitSection(deviceInformation) + MuseAboutDetailsSection(deviceInformation) + } + + Button(action: { + disconnectClosure() + dismiss() + }) { + Text("Disconnect") + .frame(maxWidth: .infinity) + } + .disabled(!state.associatedConnection) + } + .navigationTitle(Text(verbatim: model)) + .navigationBarTitleDisplayMode(.inline) + .onChange(of: state) { + if state == .disconnected { + dismiss() + } + } + } + + + init(model: String, state: ConnectionState, _ deviceInformation: MuseDeviceInformation?, disconnect: @escaping () -> Void) { + self.model = model + self.state = state + self.deviceInformation = deviceInformation + self.disconnectClosure = disconnect + } +} + + +#if DEBUG +#Preview { + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .connected, + .init( + serialNumber: "0xAABBCCDD", + firmwareVersion: "1.0", + hardwareVersion: "20.0", + remainingBatteryPercentage: 75, + wearingHeadband: true, + fit: HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .good) + ) + ) { + print("Disconnect Device") + } + } +} + +#Preview { + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .connected, + .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", hardwareVersion: "20.0", remainingBatteryPercentage: 75) + ) { + print("Disconnect Device") + } + } +} + +#Preview { + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .interventionRequired("INTERVENTION_MUSE_FIRMWARE"), + .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", hardwareVersion: "20.0", remainingBatteryPercentage: 75) + ) { + print("Disconnect Device") + } + } +} + +#Preview { + NavigationStack { + MuseDeviceDetailsView( + model: "Mock Device", + state: .disconnected, + nil + ) { + print("Disconnect Device") + } + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift new file mode 100644 index 0000000..c4ac75b --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift @@ -0,0 +1,94 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseHeadbandFitSection: View { + private let deviceInformation: MuseDeviceInformation + + + var body: some View { + Section { + ListRow("WEARING") { + if deviceInformation.wearingHeadband { + Text("Yes") + } else { + Text("No") + } + } + + if deviceInformation.wearingHeadband, + let fit = deviceInformation.fit { + NavigationLink { + MuseHeadbandFitView(fit) + } label: { + ListRow("HEADBAND_FIT") { + FitLabel(fit.overallFit) + } + } + } + } header: { + Text("HEADBAND") + } footer: { + MuseHeadbandFitProblemsHint() + } + } + + + init(_ deviceInformation: MuseDeviceInformation) { + self.deviceInformation = deviceInformation + } +} + + +#if DEBUG +#Preview { + let fit = HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .good, tp10Fit: .good) + return NavigationStack { + List { + MuseHeadbandFitSection( + .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1", wearingHeadband: true, fit: fit) + ) + } + } +} + +#Preview { + let fit = HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .good, tp10Fit: .mediocre) + return NavigationStack { + List { + MuseHeadbandFitSection( + .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1", wearingHeadband: true, fit: fit) + ) + } + } +} + +#Preview { + let fit = HeadbandFit(tp9Fit: .poor, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .mediocre) + return NavigationStack { + List { + MuseHeadbandFitSection( + .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1", wearingHeadband: true, fit: fit) + ) + } + } +} + +#Preview { + let fit = HeadbandFit(tp9Fit: .poor, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .mediocre) + return NavigationStack { + List { + MuseHeadbandFitSection( + .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1") + ) + } + } +} +#endif diff --git a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift new file mode 100644 index 0000000..b1d760b --- /dev/null +++ b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift @@ -0,0 +1,56 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseHeadbandFitView: View { + private let fit: HeadbandFit + + + var body: some View { + List { + ListRow("Overall") { + FitLabel(fit.overallFit) + } + + Section { + ListRow("TP9") { + FitLabel(fit.tp9Fit) + } + ListRow("AF7") { + FitLabel(fit.af7Fit) + } + ListRow("AF8") { + FitLabel(fit.af8Fit) + } + ListRow("TP10") { + FitLabel(fit.tp10Fit) + } + } header: { + Text("Channels") + } footer: { + MuseHeadbandFitProblemsHint() + } + } + .navigationTitle("Headband Fit") + .navigationBarTitleDisplayMode(.inline) + } + + + init(_ fit: HeadbandFit) { + self.fit = fit + } +} + + +#Preview { + NavigationStack { + MuseHeadbandFitView(.init(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .good, tp10Fit: .poor)) + } +} diff --git a/NAMS/Devices/Muse/Views/Hints/MuseBatteryProblemsHint.swift b/NAMS/Devices/Muse/Views/Hints/MuseBatteryProblemsHint.swift new file mode 100644 index 0000000..31a9ddf --- /dev/null +++ b/NAMS/Devices/Muse/Views/Hints/MuseBatteryProblemsHint.swift @@ -0,0 +1,31 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseBatteryProblemsHint: View { + @Environment(\.locale) + private var locale + + var body: some View { + HStack { + let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" + Text("PROBLEMS_BATTERY_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Muse-Battery-Troubleshooting?language=\(locale.identifier))") + } + } + + init() {} +} + + +#if DEBUG +#Preview { + MuseBatteryProblemsHint() +} +#endif diff --git a/NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift b/NAMS/Devices/Muse/Views/Hints/MuseConnectingProblemsHint.swift similarity index 90% rename from NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift rename to NAMS/Devices/Muse/Views/Hints/MuseConnectingProblemsHint.swift index f83df59..cf2acea 100644 --- a/NAMS/Devices/Muse/Views/MuseTroublesConnectingHint.swift +++ b/NAMS/Devices/Muse/Views/Hints/MuseConnectingProblemsHint.swift @@ -22,3 +22,10 @@ struct MuseTroublesConnectingHint: View { init() {} } + + +#if DEBUG +#Preview { + MuseTroublesConnectingHint() +} +#endif diff --git a/NAMS/Devices/Muse/Views/Hints/MuseHeadbandFitProblemsHint.swift b/NAMS/Devices/Muse/Views/Hints/MuseHeadbandFitProblemsHint.swift new file mode 100644 index 0000000..1f22746 --- /dev/null +++ b/NAMS/Devices/Muse/Views/Hints/MuseHeadbandFitProblemsHint.swift @@ -0,0 +1,31 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseHeadbandFitProblemsHint: View { + @Environment(\.locale) + private var locale + + var body: some View { + HStack { + let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" + Text("PROBLEMS_HEADBAND_FIT_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Sensor-Quality-Troubleshooting?language=\(locale.identifier))") + } + } + + init() {} +} + + +#if DEBUG +#Preview { + MuseHeadbandFitProblemsHint() +} +#endif diff --git a/NAMS/Devices/Muse/Views/Hints/MuseInterventionRequiredHint.swift b/NAMS/Devices/Muse/Views/Hints/MuseInterventionRequiredHint.swift new file mode 100644 index 0000000..b982de1 --- /dev/null +++ b/NAMS/Devices/Muse/Views/Hints/MuseInterventionRequiredHint.swift @@ -0,0 +1,46 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct MuseInterventionRequiredHint: View { + private let message: LocalizedStringResource + + var body: some View { + VStack { + // swiftlint:disable:next accessibility_label_for_image + let image = Image(systemName: "exclamationmark.triangle.fill") + .symbolRenderingMode(.multicolor) + Text("\(image) ", comment: "Image prefix placeholder") // this cannot be verbatim + + Text("INTERVENTION_REQUIRED_TITLE") + .fontWeight(.semibold) + + Text(verbatim: "\n") + + Text(message) + } + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .accessibilityRepresentation { + Text("INTERVENTION_REQUIRED_PREFIX \(message)") + } + } + + + init(_ message: LocalizedStringResource) { + self.message = message + } +} + + +#if DEBUG +#Preview { + MuseInterventionRequiredHint("INTERVENTION_MUSE_FIRMWARE") +} +#endif diff --git a/NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift b/NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift deleted file mode 100644 index f7a14e5..0000000 --- a/NAMS/Devices/Muse/Views/MuseDeviceDetailsView.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct MuseDeviceDetailsView: View { - private let model: String - private let state: ConnectionState - private let deviceInformation: MuseDeviceInformation - private let disconnectClosure: () -> Void - - @Environment(\.dismiss) - private var dismiss - @Environment(\.locale) - private var locale - - var body: some View { - List { - if case let .interventionRequired(message) = state { - interventionRequiredHeader(message: message) - } - - - battery - - headbandFit - - Section("About") { - ListRow("FIRMWARE_VERSION") { - Text(verbatim: deviceInformation.firmwareVersion) - } - ListRow("SERIAL_NUMBER") { - Text(verbatim: deviceInformation.serialNumber) - } - } - - Button(action: { - disconnectClosure() - dismiss() - }) { - Text("DISCONNECT") - .frame(maxWidth: .infinity) - } - .disabled(!state.associatedConnection) - } - .navigationTitle(Text(verbatim: model)) - .navigationBarTitleDisplayMode(.inline) - } - - @ViewBuilder private var battery: some View { - if let remainingBattery = deviceInformation.remainingBatteryPercentage { - Section { - ListRow("BATTERY") { - BatteryIcon(percentage: Int(remainingBattery)) - } - } footer: { - // TODO: hint separate view! - let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" - Text("PROBLEMS_BATTERY_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Muse-Battery-Troubleshooting?language=\(locale.identifier))") - } - } - } - - @ViewBuilder private var headbandFit: some View { - Section { - ListRow("WEARING") { - if deviceInformation.wearingHeadband { - Text("Yes") - } else { - Text("No") - } - } - - if deviceInformation.wearingHeadband, - let fit = deviceInformation.fit { - ListRow("HEADBAND_FIT") { // TODO: detailed fit! - let overallFit = fit.overallFit - Text(overallFit.localizedStringResource) - .foregroundStyle(overallFit.style) - } - } - } header: { - Text("HEADBAND") - } footer: { - // TODO: hint separate view! - let troubleshooting: LocalizedStringResource = "TROUBLESHOOTING" - Text("PROBLEMS_HEADBAND_FIT_HINT") + Text(" [\(troubleshooting)](https://choosemuse.my.site.com/s/article/Sensor-Quality-Troubleshooting?language=\(locale.identifier))") - } - } - - - init(model: String, state: ConnectionState, _ deviceInformation: MuseDeviceInformation, disconnect: @escaping () -> Void) { - self.model = model - self.state = state - self.deviceInformation = deviceInformation - self.disconnectClosure = disconnect - } - - - @ViewBuilder - func interventionRequiredHeader(message: LocalizedStringResource) -> some View { - VStack { - // swiftlint:disable:next accessibility_label_for_image - let image = Image(systemName: "exclamationmark.triangle.fill") - .symbolRenderingMode(.multicolor) - Text("\(image) ", comment: "Image prefix placeholder") // this cannot verbatim - + Text("INTERVENTION_REQUIRED_TITLE") - .fontWeight(.semibold) - + Text(verbatim: "\n") - + Text(message) - } - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .accessibilityRepresentation { - Text("INTERVENTION_REQUIRED_PREFIX \(message)") - } - } -} - - -#if DEBUG -#Preview { - NavigationStack { - MuseDeviceDetailsView( - model: "Mock Device", - state: .connected, - .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", remainingBatteryPercentage: 75) - ) { - print("Disconnect Device") - } - } -} - - -#Preview { - NavigationStack { - MuseDeviceDetailsView( - model: "Mock Device", - state: .connected, - .init( - serialNumber: "0xAABBCCDD", - firmwareVersion: "1.0", - remainingBatteryPercentage: 75, - wearingHeadband: true, - fit: HeadbandFit(tp9Fit: .good, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .good) - ) - ) { - print("Disconnect Device") - } - } -} - -#Preview { - NavigationStack { - MuseDeviceDetailsView( - model: "Mock Device", - state: .interventionRequired("INTERVENTION_MUSE_FIRMWARE"), - .init(serialNumber: "0xAABBCCDD", firmwareVersion: "1.0", remainingBatteryPercentage: 75) - ) { - print("Disconnect Device") - } - } -} -#endif diff --git a/NAMS/Devices/Muse/Views/MuseDeviceList.swift b/NAMS/Devices/Muse/Views/MuseDeviceList.swift index a0f4545..6708f34 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceList.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceList.swift @@ -24,18 +24,3 @@ struct MuseDeviceList: View { init() {} } #endif - - -/* - //TODO: move to mock preview? -#if DEBUG -#Preview { - NavigationStack { - List { - MuseDeviceList() - } - } - .environment(EEGViewModel(deviceManager: MockDeviceManager(immediate: true))) -} -#endif -*/ diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift index c4f3f9c..c4df55c 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -24,71 +24,20 @@ struct MuseDeviceRow: View { await deviceCoordinator.tapDevice(.muse(device)) } } secondaryAction: { - // TODO: we assume I button only shows if this is true! - if device.state == .connected { - presentingActiveDevice = device - } + presentingActiveDevice = device } .navigationDestination(item: $presentingActiveDevice) { device in - // TODO: should be always true??: Maybe just forward the optional (handles device disconnecting in the meantime!) - if let info = device.deviceInformation { - MuseDeviceDetailsView(model: device.model, state: device.connectionState, info) { - device.disconnect() - // TODO: reconsider this archotecture to catch external disconnects - deviceCoordinator.hintDisconnect() - } + MuseDeviceDetailsView(model: device.model, state: device.connectionState, device.deviceInformation) { + device.disconnect() + // TODO: reconsider this architecture to catch external disconnects + deviceCoordinator.hintDisconnect() } } } - init(device: MuseDevice) { // TODO: we could make this generic bluetooth peripheral? + init(device: MuseDevice) { self.device = device } } #endif - - -/* -// TODO: replace within MockDeviceRow! -#if DEBUG -#Preview { - NavigationStack { - List { - // TODO : EEGDeviceRow(device: MockEEGDevice(name: "Nearby Device", model: "Mock")) - } - .environment(EEGViewModel(deviceManager: MockDeviceManager())) - } -} - -#Preview { - let device = MockDevice(name: "Device 1", model: "Mock", state: .connecting) - return NavigationStack { - List { - // TODO : EEGDeviceRow(device: device) - } - } - .environment(EEGViewModel(mock: device)) -} - -#Preview { - let device = MockDevice(name: "Device 2", model: "Mock", state: .connected) - return NavigationStack { - List { - // TODO : EEGDeviceRow(device: device) - } - .environment(EEGViewModel(mock: device)) - } -} - -#Preview { - let device = MockDevice(name: "Device 3", model: "Mock", state: .interventionRequired("Firmware update required.")) - return NavigationStack { - List { - // TODO : EEGDeviceRow(device: device) - } - } - .environment(EEGViewModel(mock: device)) -} -#endif -*/ diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index 44c7a85..f70213a 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -66,13 +66,17 @@ struct EEGRecording: View { } } .onAppear { + #if MUSE if case .muse = deviceCoordinator.connectedDevice { frequency = .theta } + #endif } .onDisappear { // TODO: discarding confirmation? - eegModel.stopRecordingSession() + Task { + try await eegModel.stopRecordingSession() + } } .toolbar { Button("Close") { diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift index f381026..cd274e0 100644 --- a/NAMS/EEG/EEGRecordings.swift +++ b/NAMS/EEG/EEGRecordings.swift @@ -39,10 +39,10 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { } @MainActor - func stopRecordingSession() { - self.recordingSession = nil + func stopRecordingSession() async throws { if let device = deviceCoordinator.connectedDevice { - device.stopRecording() // TODO async? + try await device.stopRecording() } + self.recordingSession = nil } } diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 5a7c328..4acc586 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -19,8 +19,6 @@ struct HomeView: View { case mockUpload } - // TODO: EEGViewModel should be here? - @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule @AppStorage(StorageKeys.selectedPatient) @@ -31,6 +29,7 @@ struct HomeView: View { @Environment(BiopotDevice.self) private var biopot: BiopotDevice? + // TODO: how to toggle mock device manager? @State var mockDeviceManager = MockDeviceManager() #if MUSE @State var museDeviceManager = MuseDeviceManager() diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index 8e739fd..8b186cc 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -65,9 +65,6 @@ } } } - }, - "%u °C" : { - }, "About" : { "localizations" : { @@ -78,9 +75,6 @@ } } } - }, - "Actions" : { - }, "Add Notes" : { "localizations" : { @@ -102,6 +96,12 @@ } } } + }, + "AF7" : { + + }, + "AF8" : { + }, "All" : { "localizations" : { @@ -182,12 +182,6 @@ }, "Bluetooth Off" : { - }, - "Bluetooth On" : { - - }, - "Bluetooth Unauthorized" : { - }, "Bluetooth Unsupported" : { @@ -290,7 +284,7 @@ } } }, - "Charging" : { + "Channels" : { }, "Close" : { @@ -321,6 +315,7 @@ }, "CONNECTED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -334,6 +329,7 @@ }, "CONNECTING" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -418,8 +414,12 @@ } } } + }, + "Disconnect" : { + }, "DISCONNECT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -428,11 +428,9 @@ } } } - }, - "Disconnected" : { - }, "DISCONNECTED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -542,6 +540,7 @@ }, "FIRMWARE_VERSION" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -582,7 +581,11 @@ } } }, + "good" : { + "comment" : "Muse headband fit" + }, "GOOD_FIT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -604,6 +607,9 @@ } } } + }, + "Headband Fit" : { + }, "HEADBAND_FIT" : { "localizations" : { @@ -649,6 +655,7 @@ } }, "INTERVENTION_REQUIRED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -721,9 +728,6 @@ } } } - }, - "Make sure your device is connected and nearby!" : { - }, "Mark completed" : { "localizations" : { @@ -745,7 +749,11 @@ } } }, + "mediocre" : { + "comment" : "Muse headband fit" + }, "MEDIOCRE_FIT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -886,6 +894,9 @@ } } } + }, + "Overall" : { + }, "Patient Details" : { "localizations" : { @@ -948,7 +959,11 @@ } } }, + "poor" : { + "comment" : "Muse headband fit" + }, "POOR_FIT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -987,9 +1002,6 @@ } } } - }, - "Query Device Information" : { - }, "Questionnaire" : { "comment" : "Tile Type", @@ -1001,21 +1013,6 @@ } } } - }, - "Read Data Acquisition" : { - - }, - "Read Data Control" : { - - }, - "Read Device Configuration" : { - - }, - "Read Sample Configuration" : { - - }, - "Receive Device Info" : { - }, "Recording" : { "comment" : "Tile Type", @@ -1072,9 +1069,6 @@ } } } - }, - "Searching for nearby devices ..." : { - }, "Seconds" : { "localizations" : { @@ -1142,6 +1136,7 @@ }, "SERIAL_NUMBER" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1190,12 +1185,6 @@ }, "Start Recording" : { - }, - "Status" : { - - }, - "Temperature" : { - }, "The Modified Checklist for Autism in Toddlers, Revised with Follow-Up." : { "localizations" : { @@ -1227,6 +1216,12 @@ } } } + }, + "TP9" : { + + }, + "TP10" : { + }, "TROUBLESHOOTING" : { "localizations" : { @@ -1258,9 +1253,6 @@ } } } - }, - "Unknown" : { - }, "WEARING" : { "localizations" : { diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index 3055370..0a4f5ab 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -50,9 +50,6 @@ struct ScheduleView: View { .sheet(isPresented: $presentPatientSheet) { PatientListSheet(activePatientId: $activePatientId) } - .onAppear { - // TODO: biopot?.associate(eegModel) // TODO: that has to change! - } .toolbar { toolbar } diff --git a/NAMS/Tiles/ScreeningTile.swift b/NAMS/Tiles/ScreeningTile.swift index 1ac57bc..c7b3c50 100644 --- a/NAMS/Tiles/ScreeningTile.swift +++ b/NAMS/Tiles/ScreeningTile.swift @@ -34,7 +34,7 @@ struct ScreeningTile: View { } } else { SimpleTile { - tileHeader + ScreeningTileHeader(task) } footer: { Text(task.description) .font(.callout) @@ -52,32 +52,6 @@ struct ScreeningTile: View { } } - @ViewBuilder private var tileHeader: some View { - HStack { - Image(systemName: "list.bullet.clipboard") - .foregroundColor(.mint) - .font(.custom("Screening Task Icon", size: 30, relativeTo: .headline)) - .accessibilityHidden(true) - - VStack(alignment: .leading, spacing: 4) { - Text(task.title) - .font(.headline) - - HStack { - Text(task.tileType.localizedStringResource) - - Spacer() - Text("\(task.expectedCompletionMinutes) min", comment: "Expected task completion in minutes.") - .font(.subheadline) - .foregroundColor(.secondary) - } - .font(.subheadline) - .foregroundColor(.secondary) - .accessibilityElement(children: .combine) - } - } - } - init(task: ScreeningTask, presentingItem: Binding) { self.task = task diff --git a/NAMS/Tiles/ScreeningTileHeader.swift b/NAMS/Tiles/ScreeningTileHeader.swift new file mode 100644 index 0000000..04d7881 --- /dev/null +++ b/NAMS/Tiles/ScreeningTileHeader.swift @@ -0,0 +1,80 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct ScreeningTileHeader: View { + private static let iconRealignSize: DynamicTypeSize = .accessibility3 + + private let task: ScreeningTask + + @Environment(\.dynamicTypeSize) + private var dynamicTypeSize + + @State private var subheadlineAlignment: Alignment? + + var body: some View { + HStack { + if dynamicTypeSize < Self.iconRealignSize { + clipboard + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + if dynamicTypeSize >= Self.iconRealignSize { + clipboard + } + Text(task.title) + .font(.headline) + } + subheadline + } + } + } + + @ViewBuilder var clipboard: some View { + Image(systemName: "list.bullet.clipboard") + .foregroundColor(.mint) + .font(.custom("Screening Task Icon", size: 30, relativeTo: .headline)) + .accessibilityHidden(true) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) + } + + @ViewBuilder var subheadline: some View { + DynamicHStack(realignAfter: .xxxLarge, horizontalAlignment: .leading) { + Text(task.tileType.localizedStringResource) + + if subheadlineAlignment == .horizontal { + Spacer() + } + + Text("\(task.expectedCompletionMinutes) min", comment: "Expected task completion in minutes.") + } + .font(.subheadline) + .foregroundColor(.secondary) + .accessibilityElement(children: .combine) + .onPreferenceChange(Alignment.self) { alignment in + subheadlineAlignment = alignment + } + } + + + init(_ task: ScreeningTask) { + self.task = task + } +} + + +#if DEBUG +#Preview { + List { + ScreeningTileHeader(.mChatRF) + } +} +#endif diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index 4535865..195c6e1 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -12,8 +12,6 @@ import SpeziQuestionnaire import SpeziViews import SwiftUI -extension Questionnaire: Identifiable {} // TODO: move somewhere! - @MainActor struct TilesView: View { diff --git a/NAMS/Utils/ListRow.swift b/NAMS/Utils/ListRow.swift index 844719f..73f3465 100644 --- a/NAMS/Utils/ListRow.swift +++ b/NAMS/Utils/ListRow.swift @@ -27,7 +27,7 @@ extension Alignment: PreferenceKey { public struct DynamicHStack: View { // TODO: move to Spezi Views private let realignAfter: DynamicTypeSize private let verticalAlignment: VerticalAlignment - private let horizontalAlignment: HorizontalAlignment + private let horizontalAlignment: HorizontalAlignment // TODO: switch naming! private let spacing: CGFloat? private let content: Content @@ -118,7 +118,6 @@ public struct ListRow: View { } - // TODO: make arbitrary label view! public init(@ViewBuilder _ label: () -> Label, @ViewBuilder content: () -> Content) { self.label = label() self.content = content() diff --git a/NAMS/Utils/NoInformationText.swift b/NAMS/Utils/NoInformationText.swift index 5ae3e92..1ac162b 100644 --- a/NAMS/Utils/NoInformationText.swift +++ b/NAMS/Utils/NoInformationText.swift @@ -10,11 +10,12 @@ import SwiftUI +// TODO: move to SpeziViews struct NoInformationText: View { private let header: Header private let caption: Caption - var body: some View { // TODO: verify with large text and move to SpeziViews? + var body: some View { VStack { header .font(.title2) @@ -25,6 +26,7 @@ struct NoInformationText: View { .foregroundColor(.secondary) } .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) } init(@ViewBuilder header: () -> Header, @ViewBuilder caption: () -> Caption) { @@ -42,4 +44,19 @@ struct NoInformationText: View { Text(verbatim: "Please add information to show some information.") } } + +#Preview { + GeometryReader { proxy in + List { + NoInformationText { + Text(verbatim: "No Information") + } caption: { + Text(verbatim: "Please add information to show some information.") + } + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(Color.clear) + .frame(height: proxy.size.height-100) + } + } +} #endif diff --git a/NAMS/Utils/Questionnaire+Identifiable.swift b/NAMS/Utils/Questionnaire+Identifiable.swift new file mode 100644 index 0000000..689f873 --- /dev/null +++ b/NAMS/Utils/Questionnaire+Identifiable.swift @@ -0,0 +1,12 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziQuestionnaire + + +extension Questionnaire: Identifiable {} From 593991dc89ae61a7c1fea4929133406cd86cbf03 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 31 Jan 2024 19:02:36 -0800 Subject: [PATCH 05/21] Support autoconnect and a lot of other stuff --- NAMS.xcodeproj/project.pbxproj | 32 +- .../xcshareddata/swiftpm/Package.resolved | 6 +- NAMS/Devices/Biopot/BiopotDevice.swift | 102 +++---- .../Views/BiopotDeviceDetailsView.swift | 1 + .../Biopot/Views/BiopotDeviceRow.swift | 1 - NAMS/Devices/ConnectedDevice.swift | 119 ++++++++ NAMS/Devices/DeviceCoordinator.swift | 133 ++------ .../MaybeExternal/BluetoothStateHints.swift | 1 + .../MaybeExternal/LoadingSectionHeader.swift | 2 +- .../MaybeExternal/NearbyDeviceRow.swift | 14 +- NAMS/Devices/Mock/MockDevice.swift | 13 + NAMS/Devices/Mock/MockDeviceManager.swift | 14 +- NAMS/Devices/Mock/Views/MockDeviceRow.swift | 2 - NAMS/Devices/Muse/MuseDevice.swift | 10 + NAMS/Devices/Muse/MuseDeviceManager.swift | 6 +- .../Details/MuseAboutDetailsSection.swift | 1 + .../Details/MuseBatteryDetailsSection.swift | 1 + .../Details/MuseHeadbandFitSection.swift | 6 +- .../Views/Details/MuseHeadbandFitView.swift | 1 + NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 2 - NAMS/Devices/NearbyDevicesView.swift | 25 +- NAMS/EEG/Chart/EEGRecording.swift | 14 +- NAMS/EEG/Chart/StartRecordingView.swift | 2 +- NAMS/EEG/EEGRecordings.swift | 26 +- NAMS/Home.swift | 25 +- NAMS/NAMSAppDelegate.swift | 3 +- NAMS/Patients/PatientList.swift | 6 +- NAMS/Patients/PatientRow.swift | 2 +- NAMS/Resources/Localizable.xcstrings | 289 +++++++++--------- NAMS/ScheduleView.swift | 23 +- NAMS/Tiles/ScreeningTileHeader.swift | 21 +- NAMS/Tiles/TilesView.swift | 6 +- NAMS/Utils/ListRow.swift | 159 ---------- NAMS/Utils/NoInformationText.swift | 62 ---- NAMS/Utils/Testing/BiopotDevicePreview.swift | 2 +- NAMSUITests/BiopotTests.swift | 3 + ...eviceTests.swift => MockDeviceTests.swift} | 25 +- 37 files changed, 527 insertions(+), 633 deletions(-) create mode 100644 NAMS/Devices/ConnectedDevice.swift delete mode 100644 NAMS/Utils/ListRow.swift delete mode 100644 NAMS/Utils/NoInformationText.swift rename NAMSUITests/{EEGDeviceTests.swift => MockDeviceTests.swift} (79%) diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index ef86166..8f34b75 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 2DC173F479B53B9054330880 /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; 2DC174CC46386A3F7E20786B /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; 2DC174CCCA1DAC48C45CDAC4 /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; + 2DC1751E71FB3A81CDAB38BB /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */; }; 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; 2DC175D2C49F2DCB07A85C1B /* BiopotDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */; }; 2DC1762E730B0472308EEFFC /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; @@ -46,6 +47,7 @@ 2DC17A3ADA039E3FD901D8CF /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */; }; 2DC17A843968AEFAB1B64C3B /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; 2DC17ADF934F839FC66BF7A0 /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; + 2DC17B03DF6BBAC4A5C09F6D /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */; }; 2DC17B21929D86939F8EB566 /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; 2DC17B2C2E86A238A9EB9227 /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; 2DC17B4DB25C9775EF301CD0 /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; @@ -213,8 +215,6 @@ A989112D2A36687B00E66E3A /* PatientListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112C2A36687B00E66E3A /* PatientListSheet.swift */; }; A989112F2A36688A00E66E3A /* PatientRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112E2A36688A00E66E3A /* PatientRow.swift */; }; A98911322A36689D00E66E3A /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; - A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; - A9A179572AC62BE500B180D8 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A179552AC62BE500B180D8 /* ListRow.swift */; }; A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; A9BCB57D2AE7435E00DA8588 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; A9BCB57F2AE82FFC00DA8588 /* PatientSearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */; }; @@ -228,8 +228,6 @@ A9BCB58A2AE83F7E00DA8588 /* PatientListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */; }; A9BCB58C2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; A9BCB58D2AE84E6D00DA8588 /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; - A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */; }; - A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */; }; A9C82F922B608756004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F942B6089C8004703E0 /* BluetoothServices */; }; @@ -240,7 +238,7 @@ A9C82FBB2B63390E004703E0 /* BluetoothStateHints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */; }; A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; - A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */; }; + A9C9B6B42ADE191100C8C46D /* MockDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */; }; A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9D4B8D52B685D800054E27C /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */; }; @@ -327,6 +325,7 @@ 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockDeviceRow.swift; path = Views/MockDeviceRow.swift; sourceTree = ""; }; 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceList.swift; sourceTree = ""; }; 2DC17A06799FC1470D4DDC0D /* PatientList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PatientList.swift; sourceTree = ""; }; + 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; 2DC17C8A120ECD3394366958 /* MuseDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDevice.swift; sourceTree = ""; }; 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IXNMuseDataPacketType+Type.swift"; sourceTree = ""; }; 2DC17D172D8299600ED13D60 /* MockDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDevice.swift; sourceTree = ""; }; @@ -405,19 +404,17 @@ A98911312A36689D00E66E3A /* Patient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patient.swift; sourceTree = ""; }; A99522432AA61DA6009272F4 /* Muse.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Muse.framework; sourceTree = ""; }; A99522462AA61FE5009272F4 /* NAMS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NAMS-Bridging-Header.h"; sourceTree = ""; }; - A9A179552AC62BE500B180D8 /* ListRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientSearchModel.swift; sourceTree = ""; }; A9BCB5812AE8307800DA8588 /* SearchToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchToken.swift; sourceTree = ""; }; A9BCB5852AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionReference+AsyncAwait.swift"; sourceTree = ""; }; A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientListModel.swift; sourceTree = ""; }; A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentPatientLabel.swift; sourceTree = ""; }; - A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoInformationText.swift; sourceTree = ""; }; A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImpedanceMeasurement.swift; sourceTree = ""; }; A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateHints.swift; sourceTree = ""; }; A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSectionHeader.swift; sourceTree = ""; }; A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceRow.swift; sourceTree = ""; }; - A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGDeviceTests.swift; sourceTree = ""; }; + A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceTests.swift; sourceTree = ""; }; A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceDetailsView.swift; sourceTree = ""; }; A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseHeadbandFitSection.swift; sourceTree = ""; }; @@ -618,12 +615,12 @@ 653A256A28338800005D4D48 /* NAMSUITests */ = { isa = PBXGroup; children = ( + A9D83F912B081A47000D0C78 /* BiopotTests.swift */, 2F4E23862989DB360013F3D9 /* ContactsTests.swift */, - A9C9B6B32ADE191100C8C46D /* EEGDeviceTests.swift */, + A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */, 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */, A9FCE8332AE9CA4F0008EA2B /* PatientInformationTests.swift */, A91459752A4AF4E000A04641 /* QuestionnaireTests.swift */, - A9D83F912B081A47000D0C78 /* BiopotTests.swift */, ); path = NAMSUITests; sourceTree = ""; @@ -841,9 +838,7 @@ A94534152AEB0D850095AAD3 /* Errors */, 2FE5DC3D29EDD7E4004B9AB4 /* Helper */, A9DF79E82AE9B97C00AB5983 /* Testing */, - A9BCB58F2AE8588B00DA8588 /* NoInformationText.swift */, 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */, - A9A179552AC62BE500B180D8 /* ListRow.swift */, 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */, ); path = Utils; @@ -869,6 +864,7 @@ A988FEAB2B043AED00022A61 /* BatteryIcon.swift */, A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */, 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */, + 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */, ); path = Devices; sourceTree = ""; @@ -1216,7 +1212,6 @@ A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, - A9BCB5902AE8588B00DA8588 /* NoInformationText.swift in Sources */, A907DA362B1942B800FB69FB /* DataAcquisition.swift in Sources */, A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, @@ -1249,7 +1244,6 @@ A9D4B8DA2B685D800054E27C /* MuseConnectingProblemsHint.swift in Sources */, A988FEB22B0452C400022A61 /* DeviceConfiguration.swift in Sources */, 2DC17FE0AC1DD98C29B417F1 /* IXNMusePreset+Description.swift in Sources */, - A9A179562AC62BE500B180D8 /* ListRow.swift in Sources */, 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */, 2DC179D0C1180EF9B1E8F276 /* MuseDeviceManager.swift in Sources */, A94534132AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, @@ -1289,6 +1283,7 @@ 2DC171EFD6619742C29254CE /* Fit.swift in Sources */, 2DC17E6D396A8A2B50E1D44F /* Questionnaire+Identifiable.swift in Sources */, A9D4B8DB2B685D800054E27C /* MuseHeadbandFitProblemsHint.swift in Sources */, + 2DC17B03DF6BBAC4A5C09F6D /* ConnectedDevice.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1305,7 +1300,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A9C9B6B42ADE191100C8C46D /* EEGDeviceTests.swift in Sources */, + A9C9B6B42ADE191100C8C46D /* MockDeviceTests.swift in Sources */, A91459762A4AF4E000A04641 /* QuestionnaireTests.swift in Sources */, A9D83F922B081A47000D0C78 /* BiopotTests.swift in Sources */, 2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */, @@ -1350,7 +1345,6 @@ A926D78C2AB7A552000C4C2F /* Home.swift in Sources */, A94A42B32AE9EBE300A3F9E5 /* AccountButton.swift in Sources */, A926D8252AB7B430000C4C2F /* EEGSeries.swift in Sources */, - A9BCB5912AE8588B00DA8588 /* NoInformationText.swift in Sources */, A9D4B8E42B685DFA0054E27C /* MuseBatteryProblemsHint.swift in Sources */, A97E4F242B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */, A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */, @@ -1393,7 +1387,6 @@ A988FEB32B0452C400022A61 /* DeviceConfiguration.swift in Sources */, 2DC173E694F96E89EAB63FE0 /* IXNMuseConfiguration+Description.swift in Sources */, 2DC17924E7D44FE1A2562528 /* IXNMusePreset+Description.swift in Sources */, - A9A179572AC62BE500B180D8 /* ListRow.swift in Sources */, 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */, A94534142AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC1713C311BAA65EE0E2748 /* MuseDeviceManager.swift in Sources */, @@ -1430,6 +1423,7 @@ 2DC1707B04BFF1D66B3F913D /* ConnectionState.swift in Sources */, 2DC17BAFF043EBD5240BDD76 /* Fit.swift in Sources */, 2DC172F25F327CB4B718D589 /* Questionnaire+Identifiable.swift in Sources */, + 2DC1751E71FB3A81CDAB38BB /* ConnectedDevice.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2223,8 +2217,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + branch = "feature/listrow-accessibility"; + kind = branch; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ddccf0a..d975b76 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -159,7 +159,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { "branch" : "feature/unit-testing-setup", - "revision" : "e6e1b009b00463d39bfa99d3b491fa3b1b562098" + "revision" : "b31c420dd72378216c850cb8609d52636be9b9bb" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "0137e69d156bf4001a8d6bf5661c9a37b2bbd0aa", - "version" : "1.0.0" + "branch" : "feature/listrow-accessibility", + "revision" : "84898439b712b2084926ed67b1c05aa95cba56d5" } }, { diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index f4222d7..1e4af5d 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -10,24 +10,37 @@ import BluetoothServices import NIOCore import OSLog import Spezi -import SpeziBluetooth +@_spi(TestingSupport) import SpeziBluetooth // swiftlint:disable:this attributes import class CoreBluetooth.CBUUID +/// The primary Biopot service +/// +/// - Note: Notation within the docs: Access properties: R: read, W: write, N: notify. +/// Naming is currently guess work. class BiopotService: BluetoothService { - @Characteristic(id: .biopotDeviceInfoCharacteristic, notify: true) + static let id = CBUUID(string: "FFF0") + + /// Characteristic 6, as per the manual. RN. + @Characteristic(id: "FFF6", notify: true) var deviceInfo: DeviceInformation? - @Characteristic(id: .biopotDeviceConfigurationCharacteristic, notify: true) + /// Characteristic 1, as per the manual. RW. + /// Note: Even though Bluetooth reports this as notify it isn't!! + @Characteristic(id: "FFF1") var deviceConfiguration: DeviceConfiguration? - @Characteristic(id: .biopotSamplingConfigurationCharacteristic) + /// Characteristic 5, as per the manual. RW. + @Characteristic(id: "FFF5") var samplingConfiguration: SamplingConfiguration? - @Characteristic(id: .biopotDataControlCharacteristic) + /// Characteristic 2, as per the manual. RW. + @Characteristic(id: "FFF2") var dataControl: DataControl? - @Characteristic(id: .biopotImpedanceMeasurementCharacteristic) + /// Characteristic 3, as per the manual. RW. + @Characteristic(id: "FFF3") var impedanceMeasurement: ImpedanceMeasurement? - @Characteristic(id: .biopotDataAcquisitionCharacteristic) + /// Characteristic 4, as per the manual. RN. + @Characteristic(id: "FFF4", notify: true) var dataAcquisition: Data? // either `DataAcquisition10` or `DataAcquisition11` depending on the configuration. init() {} @@ -46,7 +59,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { @DeviceState(\.id) var id @DeviceState(\.state) - var state // TODO: state update doesn't really work well (sometimes stale!) + var state @DeviceState(\.name) var name @@ -55,39 +68,39 @@ class BiopotDevice: BluetoothDevice, Identifiable { @DeviceAction(\.disconnect) var disconnect - var connected: Bool { // TODO: remove? - state == .connected - } - - @Service(id: .deviceInformationService) - var deviceInformation = DeviceInformationService() // TODO: make sure we read once we are connected! - @Service(id: .biopotService) // TODO: have the service self-contained by having the id in there, allows type based discovery criteria - var service = BiopotService() + @Service var deviceInformation = DeviceInformationService() + @Service var service = BiopotService() @MainActor private var recordingSession: EEGRecordingSession? @MainActor private var startDate: Date? + @MainActor private var disconnectHandler: ((ConnectedDevice) -> Void)? required init() { service.$dataAcquisition .onChange(perform: handleDataAcquisition) - $state.onChange(perform: handleChange) + $state + .onChange(perform: handleChange) } + @MainActor private func handleChange(of state: PeripheralState) { - if case .connected = state { - Task { @MainActor in - try? await Task.sleep(for: .milliseconds(500)) // TODO: better timing? - logger.debug("Querying device information!") - do { - try await deviceInformation.retrieveDeviceInformation() - } catch { - logger.error("Failed to retrieve device information: \(error)") - } + // TODO: this is not called if the device is instantly destroyed! + logger.debug("Biopot device is now \(state)") + + if state == .disconnected || state == .disconnecting { + if let disconnectHandler { + self.disconnectHandler = nil + disconnectHandler(.biopot(self)) } } } + @MainActor + func setupDisconnectHandler(_ handler: @escaping (ConnectedDevice) -> Void) { + self.disconnectHandler = handler + } + @MainActor func startRecording(_ session: EEGRecordingSession) async throws { recordingSession = session @@ -121,7 +134,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { private func handleDataAcquisition(data: Data) { guard let deviceConfiguration = service.deviceConfiguration else { - logger.warning("Received data acquisition without having device configuration ready!") + logger.debug("Received data acquisition without having device configuration ready!") return } @@ -139,6 +152,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { } guard let acquisition else { + logger.error("Failed to decode data acquisition: \(data.hexString())") return } @@ -187,31 +201,15 @@ extension BiopotDevice: Hashable { extension BiopotDevice: GenericBluetoothPeripheral { var label: String { - // TODO: default naming? - name ?? "unknown device" // TODO: maybe strip out the mac address? + name ?? "unknown device" } - // TODO: have dedicated accessibility label? -} - - -// TODO: move that -extension CBUUID { - static let biopotService = CBUUID(string: "FFF0") - // Access properties: R: read, W: write, N: notify - // naming is currently guess work - - /// Characteristic 1, as per the manual. RWN. - static let biopotDeviceConfigurationCharacteristic = CBUUID(string: "FFF1") - /// Characteristic 2, as per the manual. RW. - static let biopotDataControlCharacteristic = CBUUID(string: "FFF2") - /// Characteristic 3, as per the manual. RW. - static let biopotImpedanceMeasurementCharacteristic = CBUUID(string: "FFF3") - /// Characteristic 4, as per the manual. RN. - static let biopotDataAcquisitionCharacteristic = CBUUID(string: "FFF4") - /// Characteristic 5, as per the manual. RW. - static let biopotSamplingConfigurationCharacteristic = CBUUID(string: "FFF5") - // swiftlint:disable:previous identifier_name - /// Characteristic 6, as per the manual. RN. - static let biopotDeviceInfoCharacteristic = CBUUID(string: "FFF6") + var accessibilityLabel: String { + let label = label + if label.starts(with: "SML BIO") { + return "SensoMedical Biopot" + } else { + return label + } + } } diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift index b80b2c1..0477a46 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index b1172d6..9cec264 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -31,7 +31,6 @@ struct BiopotDeviceRow: View { BiopotDeviceDetailsView(device: device) { Task { await device.disconnect() - deviceCoordinator.hintDisconnect() } } } diff --git a/NAMS/Devices/ConnectedDevice.swift b/NAMS/Devices/ConnectedDevice.swift new file mode 100644 index 0000000..74dd37d --- /dev/null +++ b/NAMS/Devices/ConnectedDevice.swift @@ -0,0 +1,119 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SpeziBluetooth + + +enum ConnectedDevice { + #if MUSE + case muse(_ muse: MuseDevice) + #endif + case biopot(_ biopot: BiopotDevice) + case mock(_ mock: MockDevice) + + @MainActor + func connect() async { + switch self { + #if MUSE + case let .muse(muse): + muse.connect() + #endif + case let .biopot(biopot): + await biopot.connect() + case let .mock(mock): + mock.connect() + } + } + + @MainActor + func disconnect() async { + switch self { + #if MUSE + case let .muse(muse): + muse.disconnect() + #endif + case let .biopot(biopot): + await biopot.disconnect() + case let .mock(mock): + mock.disconnect() + } + } + + @MainActor + func startRecording(_ session: EEGRecordingSession) async throws { + switch self { + #if MUSE + case let .muse(muse): + try await muse.startRecording(session) + #endif + case let .biopot(biopot): + try await biopot.startRecording(session) + case let .mock(mock): + try await mock.startRecording(session) + } + } + + @MainActor + func stopRecording() async throws { + switch self { + #if MUSE + case let .muse(muse): + try await muse.stopRecording() + #endif + case let .biopot(biopot): + try await biopot.stopRecording() + case let .mock(mock): + try await mock.stopRecording() + } + } + + @MainActor + func setupDisconnectHandler(_ handler: @escaping (ConnectedDevice) -> Void) { + switch self { + #if MUSE + case let .muse(muse): + muse.setupDisconnectHandler(handler) + #endif + case let .biopot(biopot): + biopot.setupDisconnectHandler(handler) + case let .mock(mock): + mock.setupDisconnectHandler(handler) + } + } +} + +extension ConnectedDevice: Hashable {} + + +extension ConnectedDevice: GenericBluetoothPeripheral { + var label: String { + switch self { + #if MUSE + case let .muse(muse): + muse.label + #endif + case let .biopot(biopot): + biopot.label + case let .mock(mock): + mock.label + } + } + + var state: PeripheralState { + switch self { + #if MUSE + case let .muse(muse): + muse.state + #endif + case let .biopot(biopot): + biopot.state + case let .mock(mock): + mock.state + } + } +} diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index e9da082..d6bb033 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -11,107 +11,12 @@ import OSLog import Spezi import SpeziBluetooth -enum SomeDevice { // TODO: move to separate file -#if MUSE - case muse(_ muse: MuseDevice) -#endif - case biopot(_ biopot: BiopotDevice) - case mock(_ mock: MockDevice) - - func connect() async { - switch self { -#if MUSE - case let .muse(muse): - muse.connect() -#endif - case let .biopot(biopot): - await biopot.connect() - case let .mock(mock): - mock.connect() - } - } - - func disconnect() async { - switch self { -#if MUSE - case let .muse(muse): - muse.disconnect() -#endif - case let .biopot(biopot): - await biopot.disconnect() - case let .mock(mock): - mock.disconnect() - } - } - - @MainActor - func startRecording(_ session: EEGRecordingSession) async throws { - switch self { - #if MUSE - case let .muse(muse): - try await muse.startRecording(session) - #endif - case let .biopot(biopot): - try await biopot.startRecording(session) - case let .mock(mock): - try await mock.startRecording(session) - } - } - - @MainActor - func stopRecording() async throws { - switch self { - #if MUSE - case let .muse(muse): - try await muse.stopRecording() - #endif - case let .biopot(biopot): - try await biopot.stopRecording() - case let .mock(mock): - try await mock.stopRecording() - } - } -} - -extension SomeDevice: Hashable {} - - -extension SomeDevice: GenericBluetoothPeripheral { - var label: String { - switch self { -#if MUSE - case let .muse(muse): - muse.label -#endif - case let .biopot(biopot): - biopot.label - case let .mock(mock): - mock.label - } - } - - var state: SpeziBluetooth.PeripheralState { - switch self { -#if MUSE - case let .muse(muse): - muse.state -#endif - case let .biopot(biopot): - biopot.state - case let .mock(mock): - mock.state - } - } - - // TODO: forward all the other protocol requirements!! -} - @Observable class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { let logger = Logger(subsystem: "edu.stanford.NAMS", category: "DeviceCoordinator") - private(set) var connectedDevice: SomeDevice? + private(set) var connectedDevice: ConnectedDevice? var isConnected: Bool { connectedDevice != nil @@ -127,8 +32,9 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { } + /// Device is tapped by the user in the nearby devices view. @MainActor - func tapDevice(_ device: SomeDevice) async { + func tapDevice(_ device: ConnectedDevice) async { if let connectedDevice { logger.info("Disconnecting previously connected device \(connectedDevice.label)...") // either we tapped on the same device or on another one, in any case disconnect the currently connected device @@ -144,11 +50,36 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { logger.info("Connecting to nearby device \(device.label)...") await device.connect() - self.connectedDevice = device + self.associateDevice(device) } - func hintDisconnect() { - // TODO: this must also trigger on an external disconnect! - self.connectedDevice = nil // This is not ideal right now + @MainActor + func notifyConnectedDevice(_ device: ConnectedDevice) { + if let connectedDevice { + if connectedDevice != device { + logger.info("Nearby device automatically connected, though we already have a connected device. Disconnecting it again...") + Task { + await device.disconnect() + } + } + } else { + logger.info("Nearby device automatically connected: \(device.label)") + self.associateDevice(device) + } + } + + @MainActor + private func associateDevice(_ device: ConnectedDevice) { + assert(connectedDevice == nil, "Cannot override an existing device!") + self.connectedDevice = device + device.setupDisconnectHandler { @MainActor [weak self] device in + guard let self = self, + self.connectedDevice == device else { + return + } + logger.debug("Removing association for device disconnecting in background: \(device.label).") + // TODO: deal with auto connecting muse device (after reconnect?) + self.connectedDevice = nil + } } } diff --git a/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift b/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift index e7630f4..dca9727 100644 --- a/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift +++ b/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift @@ -46,6 +46,7 @@ struct BluetoothStateHints: View { var body: some View { + // TODO: replae with ContentUnavailaleView! if titleMessage != nil || subtitleMessage != nil { VStack { if let titleMessage { diff --git a/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift b/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift index 0f3831a..bd4335e 100644 --- a/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift +++ b/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift @@ -45,7 +45,7 @@ struct LoadingSectionHeader: View { #Preview { List { Section { - Text("...") + Text(verbatim: "...") } header: { LoadingSectionHeader(verbatim: "Devices", loading: true) } diff --git a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift index ab0457b..3cd7ca4 100644 --- a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift +++ b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift @@ -7,6 +7,7 @@ // import SpeziBluetooth +import SpeziViews import SwiftUI @@ -71,7 +72,6 @@ public struct NearbyDeviceRow: View { let stack = HStack { Button(action: devicePrimaryAction) { HStack { - // TODO: allow for italics "unknown device"? ListRow(verbatim: peripheral.label) { deviceSecondaryLabel } @@ -92,15 +92,9 @@ public struct NearbyDeviceRow: View { } } - #if DEBUG - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { // TODO: verify flag! - // accessibility actions cannot be unit tested - stack - } else { - stack.accessibilityRepresentation { - accessibilityRepresentation - } - } + #if TEST + // accessibility actions cannot be unit tested + stack #else stack.accessibilityRepresentation { accessibilityRepresentation diff --git a/NAMS/Devices/Mock/MockDevice.swift b/NAMS/Devices/Mock/MockDevice.swift index df4966b..94ca26f 100644 --- a/NAMS/Devices/Mock/MockDevice.swift +++ b/NAMS/Devices/Mock/MockDevice.swift @@ -23,6 +23,7 @@ class MockDevice { /// The currently associated recording session. @MainActor private var recordingSession: EEGRecordingSession? + @MainActor private var disconnectHandler: ((ConnectedDevice) -> Void)? var connectionState: ConnectionState { switch state { @@ -49,6 +50,7 @@ class MockDevice { } + @MainActor init(name: String, state: PeripheralState = .disconnected) { self.id = UUID() self.name = name @@ -70,6 +72,12 @@ class MockDevice { } + @MainActor + func setupDisconnectHandler(_ handler: @escaping (ConnectedDevice) -> Void) { + self.disconnectHandler = handler + } + + func connect() { state = .connecting task = Task { @MainActor [weak self] in @@ -105,11 +113,16 @@ class MockDevice { } } + @MainActor func disconnect() { state = .disconnected deviceInformation = nil task = nil eegTimer = nil + if let disconnectHandler { + self.disconnectHandler = nil + disconnectHandler(.mock(self)) + } } @MainActor diff --git a/NAMS/Devices/Mock/MockDeviceManager.swift b/NAMS/Devices/Mock/MockDeviceManager.swift index b1043b8..548949b 100644 --- a/NAMS/Devices/Mock/MockDeviceManager.swift +++ b/NAMS/Devices/Mock/MockDeviceManager.swift @@ -12,12 +12,10 @@ import SpeziBluetooth @Observable class MockDeviceManager { - static var defaultNearbyDevices: [MockDevice] { - [ - MockDevice(name: "Mock Device 1"), - MockDevice(name: "Mock Device 2") - ] - } + @MainActor static let defaultNearbyDevices: [MockDevice] = [ + MockDevice(name: "Mock Device 1"), + MockDevice(name: "Mock Device 2") + ] private let storedDevicesList: [MockDevice] @@ -74,4 +72,8 @@ extension MockDeviceManager: BluetoothScanner { precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") self.startScanning() } + + func setAutoConnect(_ autoConnect: Bool) async { + precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") + } } diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift index 1ece8f4..1aec3f7 100644 --- a/NAMS/Devices/Mock/Views/MockDeviceRow.swift +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -29,8 +29,6 @@ struct MockDeviceRow: View { .navigationDestination(item: $presentingActiveDevice) { device in MuseDeviceDetailsView(model: device.label, state: device.connectionState, device.deviceInformation) { device.disconnect() - // TODO: this needs a better approach - deviceCoordinator.hintDisconnect() } } } diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift index f846748..315ecfa 100644 --- a/NAMS/Devices/Muse/MuseDevice.swift +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -78,6 +78,7 @@ class MuseDevice: Identifiable { /// The currently associated recording session. @MainActor private var recordingSession: EEGRecordingSession? + @MainActor private var disconnectHandler: ((ConnectedDevice) -> Void)? var name: String { muse.getName().replacingOccurrences(of: "Muse-", with: "") @@ -115,6 +116,11 @@ class MuseDevice: Identifiable { self.muse.setNumConnectTries(0) } + @MainActor + func setupDisconnectHandler(_ handler: @escaping (ConnectedDevice) -> Void) { + self.disconnectHandler = handler + } + func connect() { dataListener = DataListener(device: self) @@ -157,6 +163,10 @@ class MuseDevice: Identifiable { case .disconnected: deviceInformation = nil connectionListener = nil + if let disconnectHandler { + self.disconnectHandler = nil + disconnectHandler(.muse(self)) + } default: break } diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 1d7f7d3..5d5fc6b 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -21,7 +21,6 @@ class MuseDeviceManager { /// The list of nearby muse devices. private(set) var nearbyMuses: [MuseDevice] = [] - // TODO: isScanning property init() { self.museManager = IXNMuseManagerIos() @@ -41,7 +40,6 @@ class MuseDeviceManager { func stopScanning() { logger.debug("Stopped scanning for nearby Muse devices!") - // TODO: check if we are still scanning? self.museManager.stopListening() } @@ -82,6 +80,10 @@ extension MuseDeviceManager: BluetoothScanner { precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") self.startScanning() } + + func setAutoConnect(_ autoConnect: Bool) async { + precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") + } } diff --git a/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift b/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift index 018524f..d31d2ca 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseAboutDetailsSection.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift index 5aaf864..2596f3a 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseBatteryDetailsSection.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift index c4ac75b..8521d24 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI @@ -28,13 +29,13 @@ struct MuseHeadbandFitSection: View { NavigationLink { MuseHeadbandFitView(fit) } label: { - ListRow("HEADBAND_FIT") { + ListRow("Headband Fit") { FitLabel(fit.overallFit) } } } } header: { - Text("HEADBAND") + Text("Headband") } footer: { MuseHeadbandFitProblemsHint() } @@ -82,7 +83,6 @@ struct MuseHeadbandFitSection: View { } #Preview { - let fit = HeadbandFit(tp9Fit: .poor, af7Fit: .mediocre, af8Fit: .poor, tp10Fit: .mediocre) return NavigationStack { List { MuseHeadbandFitSection( diff --git a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift index b1d760b..52cbdee 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift index c4df55c..b8f4521 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -29,8 +29,6 @@ struct MuseDeviceRow: View { .navigationDestination(item: $presentingActiveDevice) { device in MuseDeviceDetailsView(model: device.model, state: device.connectionState, device.deviceInformation) { device.disconnect() - // TODO: reconsider this architecture to catch external disconnects - deviceCoordinator.hintDisconnect() } } } diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index fdb74e9..f5c82f7 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -29,7 +29,6 @@ struct NearbyDevicesView: View { @Environment(\.dismiss) private var dismiss - // TODO: implement! @AppStorage(StorageKeys.autoConnect) private var autoConnect = true @AppStorage(StorageKeys.autoConnectBackground) @@ -49,23 +48,17 @@ struct NearbyDevicesView: View { // TODO: remove closure length! // swiftlint:disable:next closure_body_length NavigationStack { - List { // swiftlint:disable:this closure_body_length + List { Section { Toggle("Auto Connect", isOn: $autoConnect) if autoConnect { - Toggle("Auto Connect Background", isOn: $autoConnectBackground) // TODO: make it a selection navigation destination? + Toggle("Continuous Background Search", isOn: $autoConnectBackground) // TODO: make it a selection navigation destination? } + } footer: { + Text("Automatically connect to SensoMedical BIOPOT3 devices.") } if consideredPoweredOn { - Section { // TODO: think about this placement? - Text("TURN_ON_HEADBAND_HINT") - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } - // TODO: sort all devices by initial discovery (descending?, latest at the top!) Section { #if MUSE @@ -85,7 +78,7 @@ struct NearbyDevicesView: View { } header: { LoadingSectionHeader("Devices", loading: isScanning) } footer: { - MuseTroublesConnectingHint() + MuseTroublesConnectingHint() // TODO: that doesn't apply to all devices? } } else { Section { @@ -101,10 +94,8 @@ struct NearbyDevicesView: View { } } } - // TODO: this probably conflicts with a global .autoConnect modifier! // TODO: auto-connect also conflicts with Muse devices? (enough to disable autoConnect if we have something connected?) - .scanNearbyDevices(with: bluetooth, autoConnect: false) // TODO: allow to dynamically disable autoConnect! - // TODO: how to handle optional modifiers? + .scanNearbyDevices(with: bluetooth, autoConnect: autoConnect && !deviceCoordinator.isConnected) .scanNearbyDevices(enabled: mockDeviceManager != nil, with: mockDeviceManager ?? MockDeviceManager()) #if MUSE .scanNearbyDevices(enabled: bluetooth.state == .poweredOn, with: museDeviceManager) @@ -130,7 +121,7 @@ struct NearbyDevicesView: View { .previewWith { DeviceCoordinator() Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } #if MUSE @@ -144,7 +135,7 @@ struct NearbyDevicesView: View { .previewWith { DeviceCoordinator() Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } #if MUSE diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index f70213a..caa1512 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -35,10 +35,10 @@ struct EEGRecording: View { var body: some View { ZStack { if !deviceCoordinator.isConnected { - NoInformationText { - Text("No Device connected!") - } caption: { - Text("Please connect to a nearby EEG headband first.") + ContentUnavailableView { + Label("No Device", systemImage: "brain.head.profile") + } description: { + Text("Please connect to a\nnearby EEG headband first.") } .navigationTitle("EEG Recording") .navigationBarTitleDisplayMode(.inline) @@ -137,7 +137,7 @@ struct EEGRecording: View { .previewWith { DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } } @@ -147,8 +147,8 @@ struct EEGRecording: View { NavigationStack { EEGRecording() .environment(PatientListModel()) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) } } @@ -158,8 +158,8 @@ struct EEGRecording: View { NavigationStack { EEGRecording() .environment(PatientListModel()) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator() } } diff --git a/NAMS/EEG/Chart/StartRecordingView.swift b/NAMS/EEG/Chart/StartRecordingView.swift index 45b88cd..4c5b83f 100644 --- a/NAMS/EEG/Chart/StartRecordingView.swift +++ b/NAMS/EEG/Chart/StartRecordingView.swift @@ -59,7 +59,7 @@ struct StartRecordingView: View { .previewWith { EEGRecordings() Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } } diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift index cd274e0..b6b1c56 100644 --- a/NAMS/EEG/EEGRecordings.swift +++ b/NAMS/EEG/EEGRecordings.swift @@ -9,8 +9,25 @@ import OSLog import Spezi +enum EEGRecordingError: LocalizedError { + case noConnectedDevice + + + var errorDescription: String? { + switch self { + case .noConnectedDevice: + String(localized: "No connected device") + } + } + + var failureReason: String? { + switch self { + case .noConnectedDevice: + String(localized: "EEG recording could not be started as no connected device was found.") + } + } +} -// TODO: search and replace .environment(EEGRecordings()) @Observable class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { @@ -28,13 +45,10 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { self.recordingSession = session guard let device = deviceCoordinator.connectedDevice else { - // TODO: throw an error! - logger.error("Tried to start EEG recording but no connected device was found!") - return + throw EEGRecordingError.noConnectedDevice } - // TODO: get current device and enable recording session? on device coordinator (so they can handle changing devices?) - // TODO: handle the case where the device disconnects when an ongoing recording is in progress? + // TODO: handle the case where the device disconnects when an ongoing recording is in progress? => Issue try await device.startRecording(session) } diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 4acc586..4e4c8b3 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -7,6 +7,7 @@ // import SpeziAccount +import SpeziBluetooth import SpeziViews import SwiftUI @@ -26,10 +27,15 @@ struct HomeView: View { @Environment(Account.self) private var account + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + @Environment(Bluetooth.self) + private var bluetooth @Environment(BiopotDevice.self) private var biopot: BiopotDevice? // TODO: how to toggle mock device manager? + @State var mockDeviceManager = MockDeviceManager() #if MUSE @State var museDeviceManager = MuseDeviceManager() @@ -40,6 +46,9 @@ struct HomeView: View { @State private var viewState: ViewState = .idle @State private var presentingAccount = false + @AppStorage(StorageKeys.autoConnectBackground) + private var autoConnectBackground = false // TODO: does this binding update? + var body: some View { TabView(selection: $selectedTab) { ScheduleView(presentingAccount: $presentingAccount, activePatientId: $activePatientId) @@ -53,12 +62,13 @@ struct HomeView: View { Label("CONTACTS_TAB_TITLE", systemImage: "person.fill") } } + .viewStateAlert(state: $viewState) .environment(patientList) .environment(mockDeviceManager) #if MUSE .environment(museDeviceManager) #endif - .viewStateAlert(state: $viewState) + .autoConnect(enabled: autoConnectBackground && !deviceCoordinator.isConnected, with: bluetooth) .onAppear { if FeatureFlags.injectDefaultPatient { Task { @@ -77,6 +87,16 @@ struct HomeView: View { patientList.removeActivePatientListener() } .onChange(of: activePatientId, handlePatientIdChange) + .onChange(of: biopot != nil) { + guard let biopot else { + return + } + + // a new device is connected now + // TODO: remove + print("NEW BIOPOT WITH STATE \(biopot.state)") + deviceCoordinator.notifyConnectedDevice(.biopot(biopot)) + } .onChange(of: viewState) { oldValue, newValue in if case .error = oldValue, case .idle = newValue { @@ -119,6 +139,9 @@ struct HomeView: View { DeviceCoordinator() EEGRecordings() AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + Bluetooth { + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) + } } } #endif diff --git a/NAMS/NAMSAppDelegate.swift b/NAMS/NAMSAppDelegate.swift index 5995d56..965b795 100644 --- a/NAMS/NAMSAppDelegate.swift +++ b/NAMS/NAMSAppDelegate.swift @@ -38,8 +38,7 @@ class NAMSAppDelegate: SpeziAppDelegate { EEGRecordings() Bluetooth { - // TODO: can this be based on the type of BiopotDevice service property? - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } } diff --git a/NAMS/Patients/PatientList.swift b/NAMS/Patients/PatientList.swift index 61606ab..4ee3d08 100644 --- a/NAMS/Patients/PatientList.swift +++ b/NAMS/Patients/PatientList.swift @@ -33,9 +33,9 @@ struct PatientList: View { let searchResults = searchModel.search(in: patients) if searchResults.isEmpty { - NoInformationText { - Text("No Patients") - } caption: { + ContentUnavailableView { + Label("No Patients", systemImage: "person.3.sequence.fill") + } description: { Text("Patients will appear here,\nonce they are added.") } } else { diff --git a/NAMS/Patients/PatientRow.swift b/NAMS/Patients/PatientRow.swift index 477cff6..cf03659 100644 --- a/NAMS/Patients/PatientRow.swift +++ b/NAMS/Patients/PatientRow.swift @@ -59,7 +59,7 @@ struct PatientRow: View { @ViewBuilder private var selectPatientButton: some View { Button(action: selectPatientAction) { - HStack { + HStack { // TODO: dynamicHStack? UserProfileView(name: patient.name) .frame(height: 30) Text(verbatim: patientName) diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index 8b186cc..8215802 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -30,9 +30,6 @@ } } } - }, - "..." : { - }, "%@ " : { "comment" : "Image prefix placeholder", @@ -98,10 +95,24 @@ } }, "AF7" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AF7" + } + } + } }, "AF8" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AF8" + } + } + } }, "All" : { "localizations" : { @@ -143,21 +154,10 @@ } } }, - "ATTENTION_REQUIRED" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Attention Required" - } - } - } - }, "Auto Connect" : { }, - "Auto Connect Background" : { + "Automatically connect to SensoMedical BIOPOT3 devices." : { }, "BATTERY" : { @@ -181,13 +181,6 @@ } }, "Bluetooth Off" : { - - }, - "Bluetooth Unsupported" : { - - }, - "BLUETOOTH_OFF" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -197,23 +190,22 @@ } } }, - "BLUETOOTH_OFF_HINT" : { + "Bluetooth Unsupported" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth is turned off. Please turn on Bluetooth in Control Center or Settings, in order to connect to a nearby device." + "value" : "Bluetooth Unsupported" } } } }, - "BLUETOOTH_PROHIBITED" : { - "extractionState" : "stale", + "BLUETOOTH_OFF_HINT" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Bluetooth Prohibited" + "value" : "Bluetooth is turned off. Please turn on Bluetooth in Control Center or Settings, in order to connect to a nearby device." } } } @@ -249,10 +241,24 @@ } }, "Brain Activity" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brain Activity" + } + } + } }, "Brain activity is visualized in real time." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brain activity is visualized in real time" + } + } + } }, "Brain activity was collected for this patient." : { "localizations" : { @@ -285,7 +291,14 @@ } }, "Channels" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Channels" + } + } + } }, "Close" : { "localizations" : { @@ -298,7 +311,14 @@ } }, "Collect the patient's brain activity using the connected EEG headband." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Collect the patient's brain activity using the connected EEG headband." + } + } + } }, "Completed" : { "comment" : "Completed Tile. Subtitle", @@ -312,10 +332,6 @@ } }, "Connected" : { - - }, - "CONNECTED" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -326,10 +342,6 @@ } }, "Connecting" : { - - }, - "CONNECTING" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -368,6 +380,9 @@ } } } + }, + "Continuous Background Search" : { + }, "Delete" : { "localizations" : { @@ -388,9 +403,6 @@ } } } - }, - "Device" : { - }, "DEVICE_DETAILS" : { "localizations" : { @@ -403,7 +415,14 @@ } }, "Devices" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devices" + } + } + } }, "Discard Changes" : { "localizations" : { @@ -416,10 +435,6 @@ } }, "Disconnect" : { - - }, - "DISCONNECT" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -429,19 +444,15 @@ } } }, - "DISCONNECTED" : { - "extractionState" : "stale", + "Disconnecting" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Disconnected" + "value" : "Disconnecting" } } } - }, - "Disconnecting" : { - }, "Done" : { "localizations" : { @@ -463,6 +474,16 @@ } } }, + "EEG recording could not be started as no connected device was found." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "EEG recording could not be started as no connected device was found." + } + } + } + }, "enter first name" : { "localizations" : { "en" : { @@ -525,22 +546,7 @@ } } }, - "Firmware update required." : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Firmware update required." - } - } - } - }, "Firmware Version" : { - - }, - "FIRMWARE_VERSION" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -582,10 +588,7 @@ } }, "good" : { - "comment" : "Muse headband fit" - }, - "GOOD_FIT" : { - "extractionState" : "stale", + "comment" : "Muse headband fit", "localizations" : { "en" : { "stringUnit" : { @@ -596,71 +599,61 @@ } }, "Hardware Version" : { - - }, - "HEADBAND" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Headband" + "value" : "Hardware Version" } } } }, - "Headband Fit" : { - - }, - "HEADBAND_FIT" : { + "Headband" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Headband Fit" + "value" : "Headband" } } } }, - "Hello" : { + "Headband Fit" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Hello" + "value" : "Headband Fit" } } } }, "Intervention Required" : { - - }, - "INTERVENTION_MUSE_FIRMWARE" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "The headband's firmware is out of date. Please use the Muse app to upgrade to the latest firmware version." + "value" : "Intervention Required" } } } }, - "INTERVENTION_MUSE_LICENSE" : { + "INTERVENTION_MUSE_FIRMWARE" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "The headband's license is invalid. Please refer to the Muse app for more information." + "value" : "The headband's firmware is out of date. Please use the Muse app to upgrade to the latest firmware version." } } } }, - "INTERVENTION_REQUIRED" : { - "extractionState" : "stale", + "INTERVENTION_MUSE_LICENSE" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Requires Intervention" + "value" : "The headband's license is invalid. Please refer to the Muse app for more information." } } } @@ -717,7 +710,14 @@ } }, "Live Visualization" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Live Visualization" + } + } + } }, "M-CHAT R/F" : { "localizations" : { @@ -750,10 +750,7 @@ } }, "mediocre" : { - "comment" : "Muse headband fit" - }, - "MEDIOCRE_FIT" : { - "extractionState" : "stale", + "comment" : "Muse headband fit", "localizations" : { "en" : { "stringUnit" : { @@ -773,53 +770,52 @@ } } }, - "MOCK_UPLOAD_TAB_TITLE" : { - "extractionState" : "stale", + "Name" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mock Web Service" + "value" : "Name" } } } }, - "Name" : { + "NEARBY_DEVICES" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Name" + "value" : "Nearby Devices" } } } }, - "NEARBY_DEVICES" : { + "No" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Nearby Devices" + "value" : "No" } } } }, - "No" : { + "No connected device" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No" + "value" : "No connected device" } } } }, - "No Device connected!" : { + "No Device" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "No Device connected!" + "value" : "No Device" } } } @@ -896,7 +892,14 @@ } }, "Overall" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Overall" + } + } + } }, "Patient Details" : { "localizations" : { @@ -949,21 +952,18 @@ } } }, - "Please connect to a nearby EEG headband first." : { + "Please connect to a\nnearby EEG headband first." : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Please connect to a nearby EEG headband first." + "value" : "Please connect to a\nnearby EEG headband first." } } } }, "poor" : { - "comment" : "Muse headband fit" - }, - "POOR_FIT" : { - "extractionState" : "stale", + "comment" : "Muse headband fit", "localizations" : { "en" : { "stringUnit" : { @@ -1026,15 +1026,11 @@ } }, "Requires Attention" : { - - }, - "Reset" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Reset" + "value" : "Requires Attention" } } } @@ -1133,10 +1129,6 @@ } }, "Serial Number" : { - - }, - "SERIAL_NUMBER" : { - "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1178,13 +1170,34 @@ } }, "Start a new EEG recording session for the currently selected patient." : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start a new EEG recording session for the currently selected patient." + } + } + } }, "Start a new Recording" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start a new Recording" + } + } + } }, "Start Recording" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Start Recording" + } + } + } }, "The Modified Checklist for Autism in Toddlers, Revised with Follow-Up." : { "localizations" : { @@ -1218,27 +1231,31 @@ } }, "TP9" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "TP9" + } + } + } }, "TP10" : { - - }, - "TROUBLESHOOTING" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Troubleshooting" + "value" : "TP10" } } } }, - "TURN_ON_HEADBAND_HINT" : { + "TROUBLESHOOTING" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Make sure your headband is turned on and nearby." + "value" : "Troubleshooting" } } } diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index 0a4f5ab..1a575e5 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -8,6 +8,7 @@ import SpeziAccount import SpeziBluetooth +import SpeziViews import SwiftUI @@ -25,19 +26,17 @@ struct ScheduleView: View { NavigationStack { ZStack { if activePatientId == nil { - VStack { - NoInformationText { - Text("No Patient selected") - } caption: { - Text("Select a patient to continue.") - } - + ContentUnavailableView { + Label("No Patient selected", systemImage: "person.fill") + } description: { + Text("Select a patient to continue.") + } actions: { Button(action: { presentPatientSheet = true }) { Text("Select Patient") } - .padding() + .padding() } } else { TilesView() @@ -90,11 +89,11 @@ struct ScheduleView: View { #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant(nil)) .environment(PatientListModel()) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator() Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } AccountConfiguration { MockUserIdPasswordAccountService() @@ -105,11 +104,11 @@ struct ScheduleView: View { #Preview { ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1")) .environment(PatientListModel()) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator() Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } AccountConfiguration { MockUserIdPasswordAccountService() diff --git a/NAMS/Tiles/ScreeningTileHeader.swift b/NAMS/Tiles/ScreeningTileHeader.swift index 04d7881..4ddcc86 100644 --- a/NAMS/Tiles/ScreeningTileHeader.swift +++ b/NAMS/Tiles/ScreeningTileHeader.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziViews import SwiftUI @@ -16,18 +17,24 @@ struct ScreeningTileHeader: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass // for iPad or landscape we want to stay horizontal - @State private var subheadlineAlignment: Alignment? + @State private var subheadlineLayout: DynamicLayout? + + private var iconGloballyPlaced: Bool { + horizontalSizeClass == .regular || dynamicTypeSize < Self.iconRealignSize + } var body: some View { HStack { - if dynamicTypeSize < Self.iconRealignSize { + if iconGloballyPlaced { clipboard } VStack(alignment: .leading, spacing: 4) { HStack { - if dynamicTypeSize >= Self.iconRealignSize { + if !iconGloballyPlaced { clipboard } Text(task.title) @@ -47,10 +54,10 @@ struct ScreeningTileHeader: View { } @ViewBuilder var subheadline: some View { - DynamicHStack(realignAfter: .xxxLarge, horizontalAlignment: .leading) { + DynamicHStack(realignAfter: .xxxLarge) { Text(task.tileType.localizedStringResource) - if subheadlineAlignment == .horizontal { + if subheadlineLayout == .horizontal { Spacer() } @@ -59,8 +66,8 @@ struct ScreeningTileHeader: View { .font(.subheadline) .foregroundColor(.secondary) .accessibilityElement(children: .combine) - .onPreferenceChange(Alignment.self) { alignment in - subheadlineAlignment = alignment + .onPreferenceChange(DynamicLayout.self) { layout in + subheadlineLayout = layout } } diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index 195c6e1..effbc70 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -102,8 +102,8 @@ struct TilesView: View { patientList.completedTasks = [] return TilesView() .environment(patientList) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) } } @@ -113,8 +113,8 @@ struct TilesView: View { patientList.completedTasks = [] return TilesView() .environment(patientList) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator() } } @@ -122,8 +122,8 @@ struct TilesView: View { #Preview { TilesView() .environment(PatientListModel()) - .environment(EEGRecordings()) .previewWith { + EEGRecordings() DeviceCoordinator() } } diff --git a/NAMS/Utils/ListRow.swift b/NAMS/Utils/ListRow.swift deleted file mode 100644 index 73f3465..0000000 --- a/NAMS/Utils/ListRow.swift +++ /dev/null @@ -1,159 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - -public enum Alignment { // TODO: better name? - case horizontal - case vertical -} - -extension Alignment: PreferenceKey { - public typealias Value = Self? - - public static func reduce(value: inout Self?, nextValue: () -> Self?) { - if let nextValue = nextValue() { - value = nextValue - } - } -} - - -public struct DynamicHStack: View { // TODO: move to Spezi Views - private let realignAfter: DynamicTypeSize - private let verticalAlignment: VerticalAlignment - private let horizontalAlignment: HorizontalAlignment // TODO: switch naming! - private let spacing: CGFloat? - private let content: Content - - @Environment(\.dynamicTypeSize) - private var dynamicTypeSize - - - public var body: some View { - if dynamicTypeSize <= realignAfter { - HStack(alignment: verticalAlignment, spacing: spacing) { - content - } - .preference(key: Alignment.self, value: .horizontal) - } else { - VStack(alignment: horizontalAlignment, spacing: spacing) { - content - } - .preference(key: Alignment.self, value: .vertical) - } - } - - - public init( - realignAfter: DynamicTypeSize = .xxLarge, - verticalAlignment: VerticalAlignment = .center, - horizontalAlignment: HorizontalAlignment = .center, - spacing: CGFloat? = nil, - @ViewBuilder content: () -> Content - ) { - self.realignAfter = realignAfter - self.verticalAlignment = verticalAlignment - self.horizontalAlignment = horizontalAlignment - self.spacing = spacing - self.content = content() - } -} - - -public struct ListRow: View { - private let label: Label - private let content: Content - - - @Environment(\.dynamicTypeSize) - private var dynamicTypeSize - @State private var alignment: Alignment? - - - public var body: some View { - HStack { - DynamicHStack(horizontalAlignment: .leading) { - label - .foregroundColor(.primary) - .lineLimit(alignment == .horizontal ? 1 : nil) - - if alignment == .horizontal { - Spacer() - } - - content - .lineLimit(alignment == .horizontal ? 1 : nil) - .layoutPriority(1) - .foregroundColor(.secondary) - } - - if alignment == .vertical { - Spacer() - } - } - .accessibilityElement(children: .combine) - .onPreferenceChange(Alignment.self) { value in - alignment = value - } - } - - - public init(verbatim label: String, @ViewBuilder content: () -> Content) where Label == Text { - self.init(label, content: content) - } - - @_disfavoredOverload - public init(_ label: String, @ViewBuilder content: () -> Content) where Label == Text { - self.init({ Text(verbatim: label) }, content: content) - } - - public init(_ label: LocalizedStringResource, @ViewBuilder content: () -> Content) where Label == Text { - self.init({ Text(label) }, content: content) - } - - - public init(@ViewBuilder _ label: () -> Label, @ViewBuilder content: () -> Content) { - self.label = label() - self.content = content() - } -} - - -#if DEBUG -#Preview { - List { - ListRow(verbatim: "Hello") { - Text(verbatim: "World") - } - - HStack { - ListRow(verbatim: "Device") { - EmptyView() - } - ProgressView() - } - - HStack { - ListRow(verbatim: "Device") { - Text(verbatim: "World") - } - ProgressView() - .padding(.leading, 6) - } - - HStack { - ListRow(verbatim: "Long Device Name") { - Text(verbatim: "Long Description") - } - ProgressView() - .padding(.leading, 4) - } - } -} -#endif diff --git a/NAMS/Utils/NoInformationText.swift b/NAMS/Utils/NoInformationText.swift deleted file mode 100644 index 1ac162b..0000000 --- a/NAMS/Utils/NoInformationText.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - - -import SwiftUI - - -// TODO: move to SpeziViews -struct NoInformationText: View { - private let header: Header - private let caption: Caption - - var body: some View { - VStack { - header - .font(.title2) - .bold() - .accessibilityAddTraits(.isHeader) - caption - .padding([.leading, .trailing], 25) - .foregroundColor(.secondary) - } - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - } - - init(@ViewBuilder header: () -> Header, @ViewBuilder caption: () -> Caption) { - self.header = header() - self.caption = caption() - } -} - - -#if DEBUG -#Preview { - NoInformationText { - Text(verbatim: "No Information") - } caption: { - Text(verbatim: "Please add information to show some information.") - } -} - -#Preview { - GeometryReader { proxy in - List { - NoInformationText { - Text(verbatim: "No Information") - } caption: { - Text(verbatim: "Please add information to show some information.") - } - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - .frame(height: proxy.size.height-100) - } - } -} -#endif diff --git a/NAMS/Utils/Testing/BiopotDevicePreview.swift b/NAMS/Utils/Testing/BiopotDevicePreview.swift index 54704cb..59b5c1f 100644 --- a/NAMS/Utils/Testing/BiopotDevicePreview.swift +++ b/NAMS/Utils/Testing/BiopotDevicePreview.swift @@ -15,7 +15,7 @@ private class PreviewDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) + Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } } } diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index e88c5a4..32475b9 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -30,6 +30,8 @@ final class BiopotTests: XCTestCase { app.navigationBars.buttons["Nearby Devices"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) + // TODO: make new tests for basic Biopot funcitonality? + /* XCTAssertTrue(app.segmentedControls.buttons["Biopot"].exists) app.segmentedControls.buttons["Biopot"].tap() @@ -40,5 +42,6 @@ final class BiopotTests: XCTestCase { XCTAssertTrue(app.staticTexts["Battery, 80 %"].exists) XCTAssertTrue(app.staticTexts["Charging, No"].exists) XCTAssertTrue(app.staticTexts["Temperature, 23 °C"].exists) + */ } } diff --git a/NAMSUITests/EEGDeviceTests.swift b/NAMSUITests/MockDeviceTests.swift similarity index 79% rename from NAMSUITests/EEGDeviceTests.swift rename to NAMSUITests/MockDeviceTests.swift index 32b2deb..3a85e8a 100644 --- a/NAMSUITests/EEGDeviceTests.swift +++ b/NAMSUITests/MockDeviceTests.swift @@ -9,7 +9,7 @@ import XCTest import XCTestExtensions -class EEGDeviceTests: XCTestCase { +class MockDeviceTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() @@ -35,23 +35,22 @@ class EEGDeviceTests: XCTestCase { app.navigationBars.buttons["Nearby Devices"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.staticTexts["Make sure your headband is turned on and nearby."].waitForExistence(timeout: 0.5)) // we don't check for the existence of the progress view as we probably cannot time that precisely - XCTAssertTrue(app.buttons["Mock, Device 1"].waitForExistence(timeout: 5.0)) - app.buttons["Mock, Device 1"].tap() + XCTAssertTrue(app.buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) + app.buttons["Mock Device 1"].tap() - XCTAssertTrue(app.buttons["Mock, Device 1, Connected"].waitForExistence(timeout: 5.0)) - XCTAssertTrue(app.buttons["Mock, Device 2"].waitForExistence(timeout: 0.5)) // ensure not connected + XCTAssertTrue(app.buttons["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.buttons["Mock Device 2"].waitForExistence(timeout: 0.5)) // ensure not connected XCTAssertTrue(app.buttons["Device Details"].waitForExistence(timeout: 2.0)) app.buttons["Device Details"].tap() // DEVICE DETAILS - XCTAssertTrue(app.navigationBars.staticTexts["Mock"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.navigationBars.staticTexts["Mock Device 1"].waitForExistence(timeout: 2.0)) XCTAssertTrue(app.staticTexts["Battery, 75 %"].waitForExistence(timeout: 0.5)) @@ -63,14 +62,14 @@ class EEGDeviceTests: XCTestCase { XCTAssertTrue(app.staticTexts["Issues maintaining a good fit? Troubleshooting"].waitForExistence(timeout: 0.5)) XCTAssertTrue(app.staticTexts["ABOUT"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Serial Number, AA BB CC DD"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Firmware Version, 1.2.0"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Serial Number, 0xAABBCCDD"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Firmware Version, 1.2"].waitForExistence(timeout: 0.5)) // DISCONNECT XCTAssertTrue(app.buttons["Disconnect"].waitForExistence(timeout: 0.5)) app.buttons["Disconnect"].tap() - XCTAssertTrue(app.buttons["Mock, Device 1"].waitForExistence(timeout: 5.0)) // ensure not connected + XCTAssertTrue(app.buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) // ensure not connected } func testEEGRecordings() { @@ -96,9 +95,9 @@ class EEGDeviceTests: XCTestCase { XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.buttons["Mock, Device 1"].waitForExistence(timeout: 5.0)) - app.buttons["Mock, Device 1"].tap() - XCTAssertTrue(app.buttons["Mock, Device 1, Connected"].waitForExistence(timeout: 5.0)) + XCTAssertTrue(app.buttons["Mock Device 1"].waitForExistence(timeout: 5.0)) + app.buttons["Mock Device 1"].tap() + XCTAssertTrue(app.buttons["Mock Device 1, Connected"].waitForExistence(timeout: 5.0)) XCTAssertTrue(app.navigationBars.buttons["Close"].waitForExistence(timeout: 0.5)) app.navigationBars.buttons["Close"].tap() From a5f7cd3f6572e5d61cd2270ecee3c6101cfd6332 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 31 Jan 2024 20:27:38 -0800 Subject: [PATCH 06/21] Move views into SpeziBluetooth --- NAMS.xcodeproj/project.pbxproj | 46 ++--- .../xcshareddata/swiftpm/Package.resolved | 6 +- NAMS/Devices/Biopot/BiopotDevice.swift | 2 +- .../Biopot/Views/BiopotDeviceRow.swift | 1 + NAMS/Devices/ConnectedDevice.swift | 1 + .../MaybeExternal/BluetoothStateHints.swift | 105 ---------- .../MaybeExternal/LoadingSectionHeader.swift | 54 ------ .../MaybeExternal/NearbyDeviceRow.swift | 181 ------------------ NAMS/Devices/Mock/MockDevice.swift | 1 + NAMS/Devices/Mock/Views/MockDeviceRow.swift | 1 + NAMS/Devices/NearbyDevicesView.swift | 5 +- NAMS/Resources/Localizable.xcstrings | 12 ++ 12 files changed, 41 insertions(+), 374 deletions(-) delete mode 100644 NAMS/Devices/MaybeExternal/BluetoothStateHints.swift delete mode 100644 NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift delete mode 100644 NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 8f34b75..4fc5a58 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -194,6 +194,8 @@ A94A42B72AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */; }; A94A42BA2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; A94A42BB2AE9ED8300A3F9E5 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; + A95EEAE32B6B54D3009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */; }; + A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */; }; A967061C2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; A967061D2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; @@ -232,12 +234,6 @@ A9C82F932B60899B004703E0 /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; A9C82F952B6089C8004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F942B6089C8004703E0 /* BluetoothServices */; }; A9C82F972B6089D2004703E0 /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A9C82F962B6089D2004703E0 /* BluetoothServices */; }; - A9C82FB72B632EE6004703E0 /* BluetoothStateHints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */; }; - A9C82FB92B633906004703E0 /* LoadingSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */; }; - A9C82FBA2B633906004703E0 /* LoadingSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */; }; - A9C82FBB2B63390E004703E0 /* BluetoothStateHints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */; }; - A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; - A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */; }; A9C9B6B42ADE191100C8C46D /* MockDeviceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */; }; A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; A9CE84522B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; @@ -411,9 +407,6 @@ A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientListModel.swift; sourceTree = ""; }; A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentPatientLabel.swift; sourceTree = ""; }; A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImpedanceMeasurement.swift; sourceTree = ""; }; - A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateHints.swift; sourceTree = ""; }; - A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSectionHeader.swift; sourceTree = ""; }; - A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceRow.swift; sourceTree = ""; }; A9C9B6B32ADE191100C8C46D /* MockDeviceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceTests.swift; sourceTree = ""; }; A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDevicesView.swift; sourceTree = ""; }; A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuseDeviceDetailsView.swift; sourceTree = ""; }; @@ -444,6 +437,7 @@ files = ( A988FEBD2B05C7AE00022A61 /* SpeziPersonalInfo in Frameworks */, 2FE5DC6429EDD883004B9AB4 /* SpeziAccount in Frameworks */, + A95EEAE32B6B54D3009B4CF8 /* BluetoothViews in Frameworks */, A988FEA62B03FB4A00022A61 /* SpeziBluetooth in Frameworks */, 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */, 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */, @@ -487,6 +481,7 @@ A926D7AD2AB7A552000C4C2F /* SpeziSecureStorage in Frameworks */, A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */, A926D7AE2AB7A552000C4C2F /* SpeziFirebaseAccount in Frameworks */, + A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */, A988FEBF2B05C7B800022A61 /* SpeziPersonalInfo in Frameworks */, A926D7B02AB7A552000C4C2F /* Spezi in Frameworks */, A926D7B12AB7A552000C4C2F /* SpeziViews in Frameworks */, @@ -844,20 +839,9 @@ path = Utils; sourceTree = ""; }; - A9C82FB52B632EC3004703E0 /* MaybeExternal */ = { - isa = PBXGroup; - children = ( - A9C82FB62B632EE6004703E0 /* BluetoothStateHints.swift */, - A9C82FB82B633906004703E0 /* LoadingSectionHeader.swift */, - A9C82FBC2B63418C004703E0 /* NearbyDeviceRow.swift */, - ); - path = MaybeExternal; - sourceTree = ""; - }; A9CE844F2B1A99FE009CE3F4 /* Devices */ = { isa = PBXGroup; children = ( - A9C82FB52B632EC3004703E0 /* MaybeExternal */, A988FEAE2B04529B00022A61 /* Biopot */, A926D8342AB7C2CC000C4C2F /* Mock */, A99522402AA61D82009272F4 /* Muse */, @@ -946,6 +930,7 @@ A988FEBC2B05C7AE00022A61 /* SpeziPersonalInfo */, A988FEA52B03FB4A00022A61 /* SpeziBluetooth */, A9C82F942B6089C8004703E0 /* BluetoothServices */, + A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */, ); productName = NAMS; productReference = 653A254D283387FE005D4D48 /* NAMS.app */; @@ -1022,6 +1007,7 @@ A988FEBE2B05C7B800022A61 /* SpeziPersonalInfo */, A988FEA72B03FB5A00022A61 /* SpeziBluetooth */, A9C82F962B6089D2004703E0 /* BluetoothServices */, + A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */, ); productName = NAMS; productReference = A926D7C32AB7A552000C4C2F /* NAMS Muse.app */; @@ -1196,7 +1182,6 @@ A94A42B62AE9EBE300A3F9E5 /* AccountSetupHeader.swift in Sources */, A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */, A9F2ECC62AEB27B10057C7DD /* MeasurementTile.swift in Sources */, - A9C82FB72B632EE6004703E0 /* BluetoothStateHints.swift in Sources */, A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */, A926D8202AB7B430000C4C2F /* EEGChannelMark.swift in Sources */, A916ADD92AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift in Sources */, @@ -1260,9 +1245,7 @@ 2DC17C29AA0382E9F5F2AA4D /* MockMeasurementGenerator.swift in Sources */, 2DC17F5243570D8FF743EADD /* PatientInformation.swift in Sources */, 2DC17C341155F06C17225169 /* NewPatientModel.swift in Sources */, - A9C82FBD2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, A9F2ECC92AEC2C300057C7DD /* CompletedTile.swift in Sources */, - A9C82FB92B633906004703E0 /* LoadingSectionHeader.swift in Sources */, A9D4B8D52B685D800054E27C /* MuseDeviceDetailsView.swift in Sources */, 2DC1718A3F968CF02D7AF0EC /* PatientList.swift in Sources */, 2DC172753D306AE733D7FDC8 /* TileType.swift in Sources */, @@ -1314,10 +1297,8 @@ buildActionMask = 2147483647; files = ( A907DA3D2B195ED800FB69FB /* EEGSample.swift in Sources */, - A9C82FBA2B633906004703E0 /* LoadingSectionHeader.swift in Sources */, A94534012AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */, A94A42AF2AE9EBE300A3F9E5 /* AccountSheet.swift in Sources */, - A9C82FBB2B63390E004703E0 /* BluetoothStateHints.swift in Sources */, A926D8002AB7B41C000C4C2F /* IXNMuseModel+Description.swift in Sources */, A9BCB5872AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift in Sources */, A926D77F2AB7A552000C4C2F /* StorageKeys.swift in Sources */, @@ -1402,7 +1383,6 @@ A9DF79E12AE8A82300AB5983 /* PatientRow.swift in Sources */, A9D4B8E12B685DF30054E27C /* MuseInterventionRequiredHint.swift in Sources */, 2DC176E7B29173393F43A357 /* MockDevice.swift in Sources */, - A9C82FBE2B63418C004703E0 /* NearbyDeviceRow.swift in Sources */, 2DC179F4A6B69C07C1A440D2 /* MockMeasurementGenerator.swift in Sources */, A9F2ECCA2AEC2C300057C7DD /* CompletedTile.swift in Sources */, 2DC17686B3AEB09A8F60AB8E /* PatientList.swift in Sources */, @@ -2217,8 +2197,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { - branch = "feature/listrow-accessibility"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 1.1.1; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -2445,6 +2425,16 @@ package = A92E34EE2ADB9B7E00FE0B51 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; + A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothViews; + }; + A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothViews; + }; A988FEA52B03FB4A00022A61 /* SpeziBluetooth */ = { isa = XCSwiftPackageProductDependency; package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d975b76..8165135 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -159,7 +159,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { "branch" : "feature/unit-testing-setup", - "revision" : "b31c420dd72378216c850cb8609d52636be9b9bb" + "revision" : "ba022198596e3d8e6573ef0868ea8089aa0d22dc" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "branch" : "feature/listrow-accessibility", - "revision" : "84898439b712b2084926ed67b1c05aa95cba56d5" + "revision" : "138d3326ca348761ebb3fe5d275dd5c89a1cc51d", + "version" : "1.1.1" } }, { diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index 1e4af5d..848dc50 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -7,7 +7,7 @@ // import BluetoothServices -import NIOCore +import BluetoothViews import OSLog import Spezi @_spi(TestingSupport) import SpeziBluetooth // swiftlint:disable:this attributes diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index 9cec264..fc3b5d3 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import SwiftUI diff --git a/NAMS/Devices/ConnectedDevice.swift b/NAMS/Devices/ConnectedDevice.swift index 74dd37d..41dbd13 100644 --- a/NAMS/Devices/ConnectedDevice.swift +++ b/NAMS/Devices/ConnectedDevice.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import SpeziBluetooth diff --git a/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift b/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift deleted file mode 100644 index dca9727..0000000 --- a/NAMS/Devices/MaybeExternal/BluetoothStateHints.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2024 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SpeziBluetooth -import SwiftUI - - -struct BluetoothStateHints: View { - private let state: BluetoothState - - - private var titleMessage: LocalizedStringResource? { - switch state { - case .poweredOn: - nil - case .poweredOff: - "Bluetooth Off" - case .unauthorized: - "Bluetooth Prohibited" - case .unsupported: - "Bluetooth Unsupported" - case .unknown: - "Bluetooth Failure" - } - } - - private var subtitleMessage: LocalizedStringResource? { - switch state { - case .poweredOn: - nil - case .poweredOff: - "BLUETOOTH_OFF_HINT" - case .unauthorized: - "BLUETOOTH_PROHIBITED_HINT" - case .unknown: - "BLUETOOTH_UNKNOWN" - case .unsupported: - "BLUETOOTH_UNSUPPORTED" - } - } - - - var body: some View { - // TODO: replae with ContentUnavailaleView! - if titleMessage != nil || subtitleMessage != nil { - VStack { - if let titleMessage { - Text(titleMessage) - .bold() - .font(.title2) - .padding(.bottom, 8) - .accessibilityAddTraits(.isHeader) - } - - if let subtitleMessage { - Text(subtitleMessage) - .multilineTextAlignment(.center) - } - } - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: -15, leading: 0, bottom: 0, trailing: 0)) - .padding([.top, .leading, .trailing]) - .frame(maxWidth: .infinity) - } else { - EmptyView() - } - } - - - init(state: BluetoothState) { - self.state = state - } -} - - -#if DEBUG -#Preview { - List { - BluetoothStateHints(state: .poweredOff) - } -} - -#Preview { - List { - BluetoothStateHints(state: .unauthorized) - } -} - -#Preview { - List { - BluetoothStateHints(state: .unsupported) - } -} - -#Preview { - List { - BluetoothStateHints(state: .unknown) - } -} -#endif diff --git a/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift b/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift deleted file mode 100644 index bd4335e..0000000 --- a/NAMS/Devices/MaybeExternal/LoadingSectionHeader.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2024 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct LoadingSectionHeader: View { - private let text: Text - private let loading: Bool - - var body: some View { - HStack { - text - if loading { - ProgressView() - .padding(.leading, 4) - .accessibilityRemoveTraits(.updatesFrequently) - } - } - } - - @_disfavoredOverload - init(verbatim: String, loading: Bool) { - self.init(Text(verbatim), loading: loading) - } - - init(_ title: LocalizedStringResource, loading: Bool) { - self.init(Text(title), loading: loading) - } - - - init(_ text: Text, loading: Bool) { - self.text = text - self.loading = loading - } -} - - -#if DEBUG -#Preview { - List { - Section { - Text(verbatim: "...") - } header: { - LoadingSectionHeader(verbatim: "Devices", loading: true) - } - } -} -#endif diff --git a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift b/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift deleted file mode 100644 index 3cd7ca4..0000000 --- a/NAMS/Devices/MaybeExternal/NearbyDeviceRow.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project -// -// SPDX-FileCopyrightText: 2024 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import SpeziBluetooth -import SpeziViews -import SwiftUI - - -public protocol GenericBluetoothPeripheral { - var label: String { get } - - var accessibilityLabel: String { get } - - var state: PeripheralState { get } - - var requiresUserAttention: Bool { get } -} - - -extension GenericBluetoothPeripheral { - public var accessibilityLabel: String { - label - } - - public var requiresUserAttention: Bool { - false - } -} - - -struct MockBluetoothDevice: GenericBluetoothPeripheral { - var label: String - var state: PeripheralState - var requiresUserAttention: Bool - - init(label: String, state: PeripheralState, requiresUserAttention: Bool = false) { - self.label = label - self.state = state - self.requiresUserAttention = requiresUserAttention - } -} - - -public struct NearbyDeviceRow: View { - private let peripheral: any GenericBluetoothPeripheral - private let devicePrimaryActionClosure: () -> Void - private let secondaryActionClosure: (() -> Void)? - - - var localizationSecondaryLabel: LocalizedStringResource? { - if peripheral.requiresUserAttention { - return "Intervention Required" - } - switch peripheral.state { - case .connecting: - return "Connecting" - case .connected: - return "Connected" - case .disconnecting: - return "Disconnecting" - case .disconnected: - return nil - } - } - - public var body: some View { - let stack = HStack { - Button(action: devicePrimaryAction) { - HStack { - ListRow(verbatim: peripheral.label) { - deviceSecondaryLabel - } - if peripheral.state == .connecting || peripheral.state == .disconnecting { - ProgressView() - .accessibilityRemoveTraits(.updatesFrequently) - } - } - } - // .frame(maxWidth: .infinity) // required for UI tests // TODO: does this break stuff? yes breaks visuals? - - if secondaryActionClosure != nil, case .connected = peripheral.state { - Button("DEVICE_DETAILS", systemImage: "info.circle", action: deviceDetailsAction) - .labelStyle(.iconOnly) - .font(.title3) - .buttonStyle(.plain) // ensure button is clickable next to the other button - .foregroundColor(.accentColor) - } - } - - #if TEST - // accessibility actions cannot be unit tested - stack - #else - stack.accessibilityRepresentation { - accessibilityRepresentation - } - #endif - } - - @ViewBuilder var accessibilityRepresentation: some View { - let button = Button(action: devicePrimaryAction) { - Text(verbatim: peripheral.accessibilityLabel) - if let localizationSecondaryLabel { - Text(localizationSecondaryLabel) - } - } - - if secondaryActionClosure != nil { - button - .accessibilityAction(named: "DEVICE_DETAILS", deviceDetailsAction) - } else { - button - } - } - - @ViewBuilder var deviceSecondaryLabel: some View { - if peripheral.requiresUserAttention { - Text("Requires Attention") - } else { - switch peripheral.state { - case .connecting, .disconnecting: - EmptyView() - case .connected: - Text("Connected") - case .disconnected: - EmptyView() - } - } - } - - - public init( - peripheral: any GenericBluetoothPeripheral, - primaryAction: @escaping () -> Void, - secondaryAction: (() -> Void)? = nil - ) { - self.peripheral = peripheral - self.devicePrimaryActionClosure = primaryAction - self.secondaryActionClosure = secondaryAction - } - - - private func devicePrimaryAction() { - devicePrimaryActionClosure() - } - - private func deviceDetailsAction() { - if let secondaryActionClosure { - secondaryActionClosure() - } - } -} - - -#if DEBUG -#Preview { - List { - NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 1", state: .connecting)) { - print("Clicked") - } secondaryAction: { - } - NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 2", state: .connected)) { - print("Clicked") - } secondaryAction: { - } - NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "Long MyDevice 3", state: .connected, requiresUserAttention: true)) { - print("Clicked") - } secondaryAction: { - } - NearbyDeviceRow(peripheral: MockBluetoothDevice(label: "MyDevice 4", state: .disconnecting)) { - print("Clicked") - } secondaryAction: { - } - } -} -#endif diff --git a/NAMS/Devices/Mock/MockDevice.swift b/NAMS/Devices/Mock/MockDevice.swift index 94ca26f..3e565a1 100644 --- a/NAMS/Devices/Mock/MockDevice.swift +++ b/NAMS/Devices/Mock/MockDevice.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import Foundation import SpeziBluetooth diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift index 1aec3f7..f829881 100644 --- a/NAMS/Devices/Mock/Views/MockDeviceRow.swift +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import SwiftUI diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index f5c82f7..7861996 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import Spezi import SpeziBluetooth import SwiftUI @@ -76,13 +77,13 @@ struct NearbyDevicesView: View { } } } header: { - LoadingSectionHeader("Devices", loading: isScanning) + LoadingSectionHeaderView("Devices", loading: isScanning) } footer: { MuseTroublesConnectingHint() // TODO: that doesn't apply to all devices? } } else { Section { - BluetoothStateHints(state: bluetooth.state) + BluetoothStateHint(bluetooth.state) } } } diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index 8215802..13a7539 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -181,6 +181,7 @@ } }, "Bluetooth Off" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -191,6 +192,7 @@ } }, "Bluetooth Unsupported" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -201,6 +203,7 @@ } }, "BLUETOOTH_OFF_HINT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -211,6 +214,7 @@ } }, "BLUETOOTH_PROHIBITED_HINT" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -221,6 +225,7 @@ } }, "BLUETOOTH_UNKNOWN" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -231,6 +236,7 @@ } }, "BLUETOOTH_UNSUPPORTED" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -332,6 +338,7 @@ } }, "Connected" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -342,6 +349,7 @@ } }, "Connecting" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -405,6 +413,7 @@ } }, "DEVICE_DETAILS" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -445,6 +454,7 @@ } }, "Disconnecting" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -629,6 +639,7 @@ } }, "Intervention Required" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1026,6 +1037,7 @@ } }, "Requires Attention" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { From 3794e553b133f8efd623a38973a221787fdf57e5 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Thu, 1 Feb 2024 21:46:18 -0800 Subject: [PATCH 07/21] Updated questionnaire and other progress --- NAMS copy-Info.plist | 19 +++ NAMS.xcodeproj/project.pbxproj | 14 +- NAMS/Devices/Muse/MuseDevice.swift | 3 +- NAMS/Devices/Muse/MuseDeviceManager.swift | 2 +- NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 1 + NAMS/Devices/NearbyDevicesView.swift | 6 +- NAMS/Home.swift | 2 +- NAMS/Patients/PatientRow.swift | 41 +++--- NAMS/Patients/Tasks/Questionnaire+NAMS.swift | 2 +- NAMS/Resources/Localizable.xcstrings | 136 +----------------- NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json | 1 - NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json | 1 + ...nse => M_CHAT_R_F-en-US-v1.1.json.license} | 0 NAMS/Utils/Testing/FeatureFlags.swift | 5 - NAMSUITests/BiopotTests.swift | 2 +- NAMSUITests/MockDeviceTests.swift | 2 +- NAMSUITests/PatientInformationTests.swift | 2 +- 17 files changed, 62 insertions(+), 177 deletions(-) create mode 100644 NAMS copy-Info.plist delete mode 100644 NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json create mode 100644 NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json rename NAMS/Resources/{M_CHAT_R_F-en-US-v1.0.json.license => M_CHAT_R_F-en-US-v1.1.json.license} (100%) diff --git a/NAMS copy-Info.plist b/NAMS copy-Info.plist new file mode 100644 index 0000000..1b53361 --- /dev/null +++ b/NAMS copy-Info.plist @@ -0,0 +1,19 @@ + + + + + ITSAppUsesNonExemptEncryption + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + UISupportedExternalAccessoryProtocols + + com.interaxon.muse + + + diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 4fc5a58..2399d72 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -165,6 +165,8 @@ A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8132AB7B430000C4C2F /* EEGReading.swift */; }; A926D82A2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; A926D82B2AB7B430000C4C2F /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; + A92BB0382B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json in Resources */ = {isa = PBXBuildFile; fileRef = A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */; }; + A92BB0392B6C204600788753 /* M_CHAT_R_F-en-US-v1.1.json in Resources */ = {isa = PBXBuildFile; fileRef = A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */; }; A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */; }; A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34F12ADB9B9000FE0B51 /* OrderedCollections */; }; A9405B562A36856300C75412 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; @@ -174,8 +176,6 @@ A94533FE2AEADCC00095AAD3 /* ScreeningTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */; }; A94534002AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; A94534012AEAE1110095AAD3 /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; - A94534062AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */ = {isa = PBXBuildFile; fileRef = A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */; }; - A94534072AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */ = {isa = PBXBuildFile; fileRef = A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */; }; A94534092AEAE3490095AAD3 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; A945340A2AEAE3490095AAD3 /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; A945340C2AEAE6380095AAD3 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340B2AEAE6380095AAD3 /* TilesView.swift */; }; @@ -374,11 +374,11 @@ A926D8122AB7B430000C4C2F /* EEGChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChannel.swift; sourceTree = ""; }; A926D8132AB7B430000C4C2F /* EEGReading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGReading.swift; sourceTree = ""; }; A926D8142AB7B430000C4C2F /* EEGChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChart.swift; sourceTree = ""; }; + A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "M_CHAT_R_F-en-US-v1.1.json"; sourceTree = ""; }; A9405B552A36856300C75412 /* AddPatientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPatientView.swift; sourceTree = ""; }; A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTile.swift; sourceTree = ""; }; A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTask.swift; sourceTree = ""; }; A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Questionnaire+NAMS.swift"; sourceTree = ""; }; - A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "M_CHAT_R_F-en-US-v1.0.json"; sourceTree = ""; }; A94534082AEAE3490095AAD3 /* ScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleView.swift; sourceTree = ""; }; A945340B2AEAE6380095AAD3 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.swift; sourceTree = ""; }; A945340F2AEAF2AE0095AAD3 /* CompletedTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTask.swift; sourceTree = ""; }; @@ -389,6 +389,7 @@ A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGRecordingSession.swift; sourceTree = ""; }; + A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy-Info.plist"; sourceTree = ""; }; A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartRecordingView.swift; sourceTree = ""; }; A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EEGChannel+Biopot.swift"; sourceTree = ""; }; A988FEA92B0414FD00022A61 /* BiopotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDevice.swift; sourceTree = ""; }; @@ -531,7 +532,7 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( - A94534052AEAE2FF0095AAD3 /* M_CHAT_R_F-en-US-v1.0.json */, + A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */, 653A255428338800005D4D48 /* Assets.xcassets */, A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */, 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */, @@ -562,6 +563,7 @@ 653A256028338800005D4D48 /* NAMSTests */, 653A256A28338800005D4D48 /* NAMSUITests */, 653A254E283387FE005D4D48 /* Products */, + A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */, ); sourceTree = ""; }; @@ -1082,7 +1084,7 @@ 653A255528338800005D4D48 /* Assets.xcassets in Resources */, A9BCB57C2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, 2F6025CB29BBE70F0045459E /* GoogleService-Info.plist in Resources */, - A94534062AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, + A92BB0382B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1104,12 +1106,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A92BB0392B6C204600788753 /* M_CHAT_R_F-en-US-v1.1.json in Resources */, A926D7B82AB7A552000C4C2F /* ConsentDocument.md in Resources */, A926D7B92AB7A552000C4C2F /* AppIcon.png in Resources */, A926D7BA2AB7A552000C4C2F /* Assets.xcassets in Resources */, A9BCB57D2AE7435E00DA8588 /* Localizable.xcstrings in Resources */, A926D7BD2AB7A552000C4C2F /* GoogleService-Info.plist in Resources */, - A94534072AEAE3000095AAD3 /* M_CHAT_R_F-en-US-v1.0.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift index 315ecfa..7567144 100644 --- a/NAMS/Devices/Muse/MuseDevice.swift +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import Foundation import OSLog import SpeziBluetooth @@ -61,7 +62,7 @@ class MuseDevice: Identifiable { .alphaAbsolute, // 8-16 Hz .betaAbsolute, // 16-32 Hz .gammaAbsolute, // 32-64 Hz - // .eeg, // enables collection of raw data + .eeg, // enables collection of raw data .hsiPrecision ] diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 5d5fc6b..2f6c70d 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -30,7 +30,7 @@ class MuseDeviceManager { logger.debug("Initialized Muse Manager with API version \(apiVersion.getString())") } - self.museManager.removeFromList(after: 6) // stale timeout if there isn't an updated advertisement + self.museManager.removeFromList(after: 10) // stale timeout if there isn't an updated advertisement } func startScanning() { diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift index b8f4521..c501f76 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import SwiftUI diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index 7861996..9ba82e4 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -31,9 +31,9 @@ struct NearbyDevicesView: View { private var dismiss @AppStorage(StorageKeys.autoConnect) - private var autoConnect = true + private var autoConnect = false @AppStorage(StorageKeys.autoConnectBackground) - private var autoConnectBackground = false + private var autoConnectBackground = false // TODO: declared twice? private var consideredPoweredOn: Bool { @@ -56,7 +56,7 @@ struct NearbyDevicesView: View { Toggle("Continuous Background Search", isOn: $autoConnectBackground) // TODO: make it a selection navigation destination? } } footer: { - Text("Automatically connect to SensoMedical BIOPOT3 devices.") + Text("Automatically connect to nearby SensoMedical BIOPOT3 devices.") } if consideredPoweredOn { diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 4e4c8b3..2e9b4f5 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -36,7 +36,7 @@ struct HomeView: View { // TODO: how to toggle mock device manager? - @State var mockDeviceManager = MockDeviceManager() + @State var mockDeviceManager: MockDeviceManager? // TODO: = MockDeviceManager() #if MUSE @State var museDeviceManager = MuseDeviceManager() #endif diff --git a/NAMS/Patients/PatientRow.swift b/NAMS/Patients/PatientRow.swift index cf03659..7ee99c4 100644 --- a/NAMS/Patients/PatientRow.swift +++ b/NAMS/Patients/PatientRow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import BluetoothViews import SpeziPersonalInfo import SpeziViews import SwiftUI @@ -35,44 +36,42 @@ struct PatientRow: View { PatientInformation(patient: patient, activePatientId: $activePatientId) } .accessibilityRepresentation { - let button = Button(action: selectPatientAction) { + Button(action: selectPatientAction) { Text(verbatim: patientName) if patient.isSelectedPatient(active: activePatientId) { Text("Selected", comment: "Selected Patient") } } - +#if !TEST + .accessibilityAction(named: "Patient Details", detailsButtonAction) +#else // accessibility actions cannot be unit tested - if !FeatureFlags.renderAccessibilityActions { - button - .accessibilityAction(named: "Patient Details", detailsButtonAction) - } else { - HStack { - button - .frame(maxWidth: .infinity) - detailsButton - .accessibilityLabel("\(patientName), Patient Details") - } - } + .frame(maxWidth: .infinity) +#endif + +#if TEST + detailsButton + .accessibilityLabel("\(patientName), Patient Details") +#endif } } @ViewBuilder private var selectPatientButton: some View { Button(action: selectPatientAction) { - HStack { // TODO: dynamicHStack? - UserProfileView(name: patient.name) - .frame(height: 30) - Text(verbatim: patientName) - .foregroundColor(.primary) - Spacer() - + ListRow { + HStack { + UserProfileView(name: patient.name) + .frame(height: 30) + Text(verbatim: patientName) + .foregroundColor(.primary) + } + } content: { if editMode?.wrappedValue.isEditing != true && patient.isSelectedPatient(active: activePatientId) { Text("Selected", comment: "Selected Patient") .foregroundColor(.secondary) } } - .frame(maxWidth: .infinity) } } diff --git a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift index 9751fbb..901cb4a 100644 --- a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift +++ b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift @@ -28,6 +28,6 @@ extension Questionnaire { extension Questionnaire { static var mChatRF: Questionnaire = { - questionnaire(withName: "M_CHAT_R_F-en-US-v1.0", bundle: .main) + questionnaire(withName: "M_CHAT_R_F-en-US-v1.1", bundle: .main) }() } diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index 13a7539..e4aec9f 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -157,7 +157,7 @@ "Auto Connect" : { }, - "Automatically connect to SensoMedical BIOPOT3 devices." : { + "Automatically connect to nearby SensoMedical BIOPOT3 devices." : { }, "BATTERY" : { @@ -180,72 +180,6 @@ } } }, - "Bluetooth Off" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth Off" - } - } - } - }, - "Bluetooth Unsupported" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth Unsupported" - } - } - } - }, - "BLUETOOTH_OFF_HINT" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth is turned off. Please turn on Bluetooth in Control Center or Settings, in order to connect to a nearby device." - } - } - } - }, - "BLUETOOTH_PROHIBITED_HINT" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth is required to make connections to a nearby device. Please allow Bluetooth connections in your Privacy settings." - } - } - } - }, - "BLUETOOTH_UNKNOWN" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "We have troubles with the Bluetooth communication.\\n\nPlease try again." - } - } - } - }, - "BLUETOOTH_UNSUPPORTED" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bluetooth is unsupported on this device!" - } - } - } - }, "Brain Activity" : { "localizations" : { "en" : { @@ -337,28 +271,6 @@ } } }, - "Connected" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected" - } - } - } - }, - "Connecting" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting" - } - } - } - }, "CONTACTS_NAVIGATION_TITLE" : { "localizations" : { "en" : { @@ -412,17 +324,6 @@ } } }, - "DEVICE_DETAILS" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device Details" - } - } - } - }, "Devices" : { "localizations" : { "en" : { @@ -453,17 +354,6 @@ } } }, - "Disconnecting" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Disconnecting" - } - } - } - }, "Done" : { "localizations" : { "en" : { @@ -638,17 +528,6 @@ } } }, - "Intervention Required" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intervention Required" - } - } - } - }, "INTERVENTION_MUSE_FIRMWARE" : { "localizations" : { "en" : { @@ -999,7 +878,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Do you have problems connecting?" + "value" : "Do you have problems connecting to your Muse device?" } } } @@ -1036,17 +915,6 @@ } } }, - "Requires Attention" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requires Attention" - } - } - } - }, "Schedule" : { "comment" : "Schedule Title", "localizations" : { diff --git a/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json b/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json deleted file mode 100644 index f53b734..0000000 --- a/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json +++ /dev/null @@ -1 +0,0 @@ -{"title":"M-CHAT R/F","resourceType":"Questionnaire","language":"en-US","name":"M_CHAT_R_F","status":"active","publisher":"Stanford Biodesign Digital Health","meta":{"profile":["http://spezi.health/fhir/StructureDefinition/sdf-Questionnaire"],"tag":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English"}]},"useContext":[{"code":{"system":"http://hl7.org/fhir/ValueSet/usage-context-type","code":"focus","display":"Clinical Focus"},"valueCodeableConcept":{"coding":[{"system":"urn:oid:2.16.578.1.12.4.1.1.8655","display":"M-CHAT R/F"}]}}],"contact":[{"name":"http://spezi.health"}],"subjectType":["Patient"],"url":"http://spezi.health/fhir/questionnaire/21a5bee6-b8ad-495b-a05b-26378c8c3b31","version":"1.0","date":"2023-10-26T00:00:00-07:00","id":"mchat-rf","item":[{"linkId":"680becad-7ecc-4909-9aa7-55b680f300b7","type":"choice","text":"If you point at something across the room, does your child look at it?\n\nFor example, if you point at a toy or an animal, does your child look at the toy or animal?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1cd27a89-5e03-4564-85f3-3ace02a5da44","type":"choice","text":"Have you ever wondered if your child might be deaf?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1da67934-bb63-421a-b770-a0009f6a19ea","type":"choice","text":"Does your child play pretend or make-believe?\n\nFor example, pretend to drink from an empty cup, pretend to talk on a phone, or pretend to feed a doll or stuffed animal?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"0fd8945c-2fa4-46a3-a3c6-8e1c8b262d77","type":"choice","text":"Does your child like climbing on things?\n\nFor example, furniture, playground equipment, or stairs.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"6302ca35-ad63-43f1-8167-1b86792484ee","type":"choice","text":"Does your child make **unusual** finger movements near his or her eyes?\n\nFor example, does your child wiggle his or her fingers close to his or her eyes?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"64012245-04b4-40b1-8362-7214e066b567","type":"choice","text":"Does your child point with one finger to ask for something or to get help?\n\nFor example, pointing to a snack or toy that is out of reach.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"9843d642-0f1f-4e0c-9fb4-11593ac2c3ce","type":"choice","text":"Does your child point with one finger to show you something interesting?\n\nFor example, pointing to an airplane in the sky or a big truck in the road.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"416d2b7b-b6ad-4778-8788-aa52d20a41cd","type":"choice","text":"Is your child interested in other children?\n\nFor example, does your child watch other children, smile at them, or go to them?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"fa145ad9-cda7-4d6a-8c8d-e18671019396","type":"choice","text":"Does your child show you things by bringing them to you or holding them up for you to see – not to get help, but just to share?\n\nFor example, showing you a flower, a stuffed animal, or a toy truck.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"facdbdd8-f77c-4d23-f4bf-0d8f91ee8fb2","type":"choice","text":"Does your child respond when you call his or her name?\n\nFor example, does your her or she look up, talk or babble, or stop what he or she is doing when you call his or her name?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"1a2dd10f-390f-41aa-f9d2-3e13c3d3d142","type":"choice","text":"When you smile at your child, does he or she smile back at you?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"36bf0bdd-6d80-4882-876e-448430fae877","type":"choice","text":"Does your child get upset by everyday noises?\n\nFor example, does your child scream or cry to noise such as a vacuum cleaner or load music?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"9eeae1fe-b858-4ccf-8b97-a933077691e9","type":"choice","text":"Does your child walk?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"b4aed134-1f35-4e83-e222-243855f38b2d","type":"choice","text":"Does your child look you in the eye when you are talking to him or her, playing with him or her, or dressing him or her?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"234916d7-ad3f-4d73-8ecd-af5e1c9e6578","type":"choice","text":"Does your child try to copy what you do?\n\nFor example, wave bye-bye, clap, or make a funny noise when you do.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"e3fc7bb4-f5a0-4f02-9422-6dded9d683d7","type":"choice","text":"If you turn your head to look at something, does your child look around to see what you are looking at?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"77c47260-d5a9-4151-824a-3fe3e880ca2e","type":"choice","text":"Does your child try to get you to watch him or her?\n\nFor example, does your child look at you for praise, or say \"look\" or \"watch me\"?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"8493c48e-63f1-4479-f324-317858391abf","type":"choice","text":"Does your child understand when you tell him or her to do something?\n\nFor example, if you don’t point, can your child understand “put the book on the chair” or “bring me the blanket”?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"59c27629-6010-4255-caf4-3a4e6e74719a","type":"choice","text":"If something new happens, does your child look at your face to see how you feel about it?\n\nFor example, if he or she hears a strange or funny noise, or sees a new toy, will\nhe or she look at your face?","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]},{"linkId":"3214425d-902f-4d98-8d09-31ee56776848","type":"choice","text":"Does your child like movement activities?\n\nFor example, being swung or bounced on your knee.","required":true,"answerOption":[{"valueCoding":{"id":"60559b51-5007-4a87-cfa1-5a2710587536","code":"yes","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"Yes"}},{"valueCoding":{"id":"ed6a98e2-6ee8-492d-ed40-6a26e6d3c72a","code":"no","system":"urn:uuid:12c2db48-dabf-4b28-8904-47a66cc66b6b","display":"No"}}]}]} \ No newline at end of file diff --git a/NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json b/NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json new file mode 100644 index 0000000..e2b925c --- /dev/null +++ b/NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json @@ -0,0 +1 @@ +{"resourceType":"Questionnaire","language":"en-US","id":"mchat-rf","name":"M_CHAT_R_F","title":"M-CHAT R/F","description":"The Modified Checklist for Autism in Toddlers, Revised with Follow-Up.","version":"1.1","status":"active","publisher":"Stanford Biodesign Digital Health","meta":{"profile":["http://spezi.health/fhir/StructureDefinition/sdf-Questionnaire"],"tag":[{"system":"urn:ietf:bcp:47","code":"en-US","display":"English"}]},"useContext":[{"code":{"system":"http://hl7.org/fhir/ValueSet/usage-context-type","code":"focus","display":"Clinical Focus"},"valueCodeableConcept":{"coding":[{"system":"urn:oid:2.16.578.1.12.4.1.1.8655","display":"M-CHAT R/F"}]}}],"contact":[{"name":"http://spezi.health"}],"subjectType":["Patient"],"date":"2024-02-01T00:00:00-08:00","url":"http://spezi.health/fhir/questionnaire/21a5bee6-b8ad-495b-a05b-26378c8c3b31","contained":[{"url":"http://spezi.health/fhir/ValueSet/Predefined","resourceType":"ValueSet","id":"1101","version":"1.0","name":"urn:oid:1101","title":"Yes / No","status":"draft","publisher":"NHN","compose":{"include":[{"system":"urn:oid:2.16.578.1.12.4.1.1101","concept":[{"code":"1","display":"Yes"},{"code":"2","display":"No"}]}]}}],"item":[{"linkId":"1d877a6d-db27-4273-d2fb-d6ca709a1024","type":"group","item":[{"linkId":"680becad-7ecc-4909-9aa7-55b680f300b7","type":"choice","text":"If you point at something across the room, does your child look at it?\n\nFor example, if you point at a toy or an animal, does your child look at the toy or animal?","required":true,"answerValueSet":"#1101"},{"linkId":"1cd27a89-5e03-4564-85f3-3ace02a5da44","type":"choice","text":"Have you ever wondered if your child might be deaf?","required":true,"answerValueSet":"#1101"},{"linkId":"1da67934-bb63-421a-b770-a0009f6a19ea","type":"choice","text":"Does your child play pretend or make-believe?\n\nFor example, pretend to drink from an empty cup, pretend to talk on a phone, or pretend to feed a doll or stuffed animal?","required":true,"answerValueSet":"#1101"},{"linkId":"0fd8945c-2fa4-46a3-a3c6-8e1c8b262d77","type":"choice","text":"Does your child like climbing on things?\n\nFor example, furniture, playground equipment, or stairs.","required":true,"answerValueSet":"#1101"},{"linkId":"6302ca35-ad63-43f1-8167-1b86792484ee","type":"choice","text":"Does your child make **unusual** finger movements near his or her eyes?\n\nFor example, does your child wiggle his or her fingers close to his or her eyes?","required":true,"answerValueSet":"#1101"},{"linkId":"64012245-04b4-40b1-8362-7214e066b567","type":"choice","text":"Does your child point with one finger to ask for something or to get help?\n\nFor example, pointing to a snack or toy that is out of reach.","required":true,"answerValueSet":"#1101"},{"linkId":"9843d642-0f1f-4e0c-9fb4-11593ac2c3ce","type":"choice","text":"Does your child point with one finger to show you something interesting?\n\nFor example, pointing to an airplane in the sky or a big truck in the road.","required":true,"answerValueSet":"#1101"},{"linkId":"416d2b7b-b6ad-4778-8788-aa52d20a41cd","type":"choice","text":"Is your child interested in other children?\n\nFor example, does your child watch other children, smile at them, or go to them?","required":true,"answerValueSet":"#1101"},{"linkId":"fa145ad9-cda7-4d6a-8c8d-e18671019396","type":"choice","text":"Does your child show you things by bringing them to you or holding them up for you to see – not to get help, but just to share?\n\nFor example, showing you a flower, a stuffed animal, or a toy truck.","required":true,"answerValueSet":"#1101"},{"linkId":"facdbdd8-f77c-4d23-f4bf-0d8f91ee8fb2","type":"choice","text":"Does your child respond when you call his or her name?\n\nFor example, does your her or she look up, talk or babble, or stop what he or she is doing when you call his or her name?","required":true,"answerValueSet":"#1101"},{"linkId":"1a2dd10f-390f-41aa-f9d2-3e13c3d3d142","type":"choice","text":"When you smile at your child, does he or she smile back at you?","required":true,"answerValueSet":"#1101"},{"linkId":"36bf0bdd-6d80-4882-876e-448430fae877","type":"choice","text":"Does your child get upset by everyday noises?\n\nFor example, does your child scream or cry to noise such as a vacuum cleaner or load music?","required":true,"answerValueSet":"#1101"},{"linkId":"9eeae1fe-b858-4ccf-8b97-a933077691e9","type":"choice","text":"Does your child walk?","required":true,"answerValueSet":"#1101"},{"linkId":"b4aed134-1f35-4e83-e222-243855f38b2d","type":"choice","text":"Does your child look you in the eye when you are talking to him or her, playing with him or her, or dressing him or her?","required":true,"answerValueSet":"#1101"},{"linkId":"234916d7-ad3f-4d73-8ecd-af5e1c9e6578","type":"choice","text":"Does your child try to copy what you do?\n\nFor example, wave bye-bye, clap, or make a funny noise when you do.","required":true,"answerValueSet":"#1101"},{"linkId":"e3fc7bb4-f5a0-4f02-9422-6dded9d683d7","type":"choice","text":"If you turn your head to look at something, does your child look around to see what you are looking at?","required":true,"answerValueSet":"#1101"},{"linkId":"77c47260-d5a9-4151-824a-3fe3e880ca2e","type":"choice","text":"Does your child try to get you to watch him or her?\n\nFor example, does your child look at you for praise, or say \"look\" or \"watch me\"?","required":true,"answerValueSet":"#1101"},{"linkId":"8493c48e-63f1-4479-f324-317858391abf","type":"choice","text":"Does your child understand when you tell him or her to do something?\n\nFor example, if you don’t point, can your child understand “put the book on the chair” or “bring me the blanket”?","required":true,"answerValueSet":"#1101"},{"linkId":"59c27629-6010-4255-caf4-3a4e6e74719a","type":"choice","text":"If something new happens, does your child look at your face to see how you feel about it?\n\nFor example, if he or she hears a strange or funny noise, or sees a new toy, will\nhe or she look at your face?","required":true,"answerValueSet":"#1101"},{"linkId":"3214425d-902f-4d98-8d09-31ee56776848","type":"choice","text":"Does your child like movement activities?\n\nFor example, being swung or bounced on your knee.","required":true,"answerValueSet":"#1101"}],"required":false}]} \ No newline at end of file diff --git a/NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json.license b/NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json.license similarity index 100% rename from NAMS/Resources/M_CHAT_R_F-en-US-v1.0.json.license rename to NAMS/Resources/M_CHAT_R_F-en-US-v1.1.json.license diff --git a/NAMS/Utils/Testing/FeatureFlags.swift b/NAMS/Utils/Testing/FeatureFlags.swift index f213894..dca5fd2 100644 --- a/NAMS/Utils/Testing/FeatureFlags.swift +++ b/NAMS/Utils/Testing/FeatureFlags.swift @@ -19,11 +19,6 @@ enum FeatureFlags { /// Defines if the application should connect to the local firebase emulator. Always set to true when using the iOS simulator. static let useFirebaseEmulator = CommandLine.arguments.contains("--useFirebaseEmulator") #endif - /// Custom accessibility actions cannot be reliably tested. This flag ensures custom accessibility actions - /// are rendered as UI elements. - static let renderAccessibilityActions = CommandLine.arguments.contains("--render-accessibility-actions") /// A default patient is injected you may use within UI tests. static let injectDefaultPatient = CommandLine.arguments.contains("--inject-default-patient") - /// Enable test specific functionality for the Biopot platform. - static let testBiopot = CommandLine.arguments.contains("--test-biopot") } diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index 32475b9..321c39d 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -16,7 +16,7 @@ final class BiopotTests: XCTestCase { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--skipOnboarding", "--test-biopot"] + app.launchArguments = ["--skipOnboarding"] app.launch() } diff --git a/NAMSUITests/MockDeviceTests.swift b/NAMSUITests/MockDeviceTests.swift index 3a85e8a..cc4dfea 100644 --- a/NAMSUITests/MockDeviceTests.swift +++ b/NAMSUITests/MockDeviceTests.swift @@ -16,7 +16,7 @@ class MockDeviceTests: XCTestCase { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--skipOnboarding", "--render-accessibility-actions", "--inject-default-patient"] + app.launchArguments = ["--skipOnboarding", "--inject-default-patient"] app.launch() } diff --git a/NAMSUITests/PatientInformationTests.swift b/NAMSUITests/PatientInformationTests.swift index 62b00af..372fd5f 100644 --- a/NAMSUITests/PatientInformationTests.swift +++ b/NAMSUITests/PatientInformationTests.swift @@ -17,7 +17,7 @@ final class PatientInformationTests: XCTestCase { continueAfterFailure = false let app = XCUIApplication() - app.launchArguments = ["--skipOnboarding", "--render-accessibility-actions", "--inject-default-patient"] + app.launchArguments = ["--skipOnboarding", "--inject-default-patient"] app.launch() } From a5694ab0693af5b7a6186af57888c15813dab607 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 2 Feb 2024 12:37:39 -0800 Subject: [PATCH 08/21] Initial vision pro support --- NAMS copy2-Info.plist | 19 + NAMS.xcodeproj/project.pbxproj | 681 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 33 +- .../xcschemes/NAMS Vision.xcscheme | 77 ++ NAMS/Home.swift | 3 +- ...tientListModel+QuestionnaireResponse.swift | 2 + NAMS/Patients/Tasks/CompletedTask.swift | 10 +- NAMS/Patients/Tasks/Questionnaire+NAMS.swift | 2 + NAMS/Patients/Tasks/ScreeningTask.swift | 2 + NAMS/Tiles/ScreeningTile.swift | 3 + NAMS/Tiles/ScreeningTileHeader.swift | 2 + NAMS/Tiles/TilesView.swift | 11 +- NAMS/Utils/Questionnaire+Identifiable.swift | 2 + 13 files changed, 828 insertions(+), 19 deletions(-) create mode 100644 NAMS copy2-Info.plist create mode 100644 NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Vision.xcscheme diff --git a/NAMS copy2-Info.plist b/NAMS copy2-Info.plist new file mode 100644 index 0000000..1b53361 --- /dev/null +++ b/NAMS copy2-Info.plist @@ -0,0 +1,19 @@ + + + + + ITSAppUsesNonExemptEncryption + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + UISupportedExternalAccessoryProtocols + + com.interaxon.muse + + + diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 2399d72..bc853b9 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -198,6 +198,135 @@ A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */; }; A967061C2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; A967061D2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; + A97A99FE2B6CB9450021D80A /* EEGSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA3B2B195ED800FB69FB /* EEGSample.swift */; }; + A97A99FF2B6CB9450021D80A /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; + A97A9A002B6CB9450021D80A /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; + A97A9A012B6CB9450021D80A /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42A92AE9EBE200A3F9E5 /* AccountSheet.swift */; }; + A97A9A022B6CB9450021D80A /* IXNMuseModel+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F52AB7B41B000C4C2F /* IXNMuseModel+Description.swift */; }; + A97A9A032B6CB9450021D80A /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; + A97A9A042B6CB9450021D80A /* SearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5812AE8307800DA8588 /* SearchToken.swift */; }; + A97A9A052B6CB9450021D80A /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; + A97A9A062B6CB9450021D80A /* ScreeningTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */; }; + A97A9A072B6CB9450021D80A /* EEGChannel+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7FA2AB7B41B000C4C2F /* EEGChannel+Muse.swift */; }; + A97A9A082B6CB9450021D80A /* EEGRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80F2AB7B430000C4C2F /* EEGRecording.swift */; }; + A97A9A092B6CB9450021D80A /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD42AB60227006960DF /* NotificationPermissions.swift */; }; + A97A9A0A2B6CB9450021D80A /* DataControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA2F2B192FD500FB69FB /* DataControl.swift */; }; + A97A9A0B2B6CB9450021D80A /* ScreeningTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */; }; + A97A9A0C2B6CB9450021D80A /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; + A97A9A0D2B6CB9450021D80A /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */; }; + A97A9A0E2B6CB9450021D80A /* MuseInterventionRequiredHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */; }; + A97A9A0F2B6CB9450021D80A /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; + A97A9A102B6CB9450021D80A /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; + A97A9A112B6CB9450021D80A /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; + A97A9A122B6CB9450021D80A /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */; }; + A97A9A132B6CB9450021D80A /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; + A97A9A142B6CB9450021D80A /* MeasurementTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */; }; + A97A9A152B6CB9450021D80A /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; + A97A9A162B6CB9450021D80A /* EEGChannelMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */; }; + A97A9A172B6CB9450021D80A /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; + A97A9A182B6CB9450021D80A /* PatientListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112C2A36687B00E66E3A /* PatientListSheet.swift */; }; + A97A9A192B6CB9450021D80A /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; + A97A9A1A2B6CB9450021D80A /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AB2AE9EBE200A3F9E5 /* AccountButton.swift */; }; + A97A9A1B2B6CB9450021D80A /* MuseBatteryDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */; }; + A97A9A1C2B6CB9450021D80A /* EEGSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8112AB7B430000C4C2F /* EEGSeries.swift */; }; + A97A9A1D2B6CB9450021D80A /* EEGChannel+Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */; }; + A97A9A1E2B6CB9450021D80A /* MuseHeadbandFitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */; }; + A97A9A1F2B6CB9450021D80A /* OnboardingFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */; }; + A97A9A202B6CB9450021D80A /* MuseBatteryProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */; }; + A97A9A212B6CB9450021D80A /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; + A97A9A222B6CB9450021D80A /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; + A97A9A232B6CB9450021D80A /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; + A97A9A242B6CB9450021D80A /* DataAcquisition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA352B1942B800FB69FB /* DataAcquisition.swift */; }; + A97A9A252B6CB9450021D80A /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; + A97A9A262B6CB9450021D80A /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; + A97A9A272B6CB9450021D80A /* DeviceInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB42B0453E100022A61 /* DeviceInformation.swift */; }; + A97A9A282B6CB9450021D80A /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; + A97A9A292B6CB9450021D80A /* CollectionReference+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5852AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift */; }; + A97A9A2A2B6CB9450021D80A /* EEGSeries+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F92AB7B41B000C4C2F /* EEGSeries+Muse.swift */; }; + A97A9A2B2B6CB9450021D80A /* NAMSTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */; }; + A97A9A2C2B6CB9450021D80A /* SamplingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA322B193C9700FB69FB /* SamplingConfiguration.swift */; }; + A97A9A2D2B6CB9450021D80A /* NAMSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */; }; + A97A9A2E2B6CB9450021D80A /* QuestionnaireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */; }; + A97A9A2F2B6CB9450021D80A /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; + A97A9A302B6CB9450021D80A /* PatientRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112E2A36688A00E66E3A /* PatientRow.swift */; }; + A97A9A312B6CB9450021D80A /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; + A97A9A322B6CB9450021D80A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */; }; + A97A9A332B6CB9450021D80A /* CompletedTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340F2AEAF2AE0095AAD3 /* CompletedTask.swift */; }; + A97A9A342B6CB9450021D80A /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEAB2B043AED00022A61 /* BatteryIcon.swift */; }; + A97A9A352B6CB9450021D80A /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; + A97A9A362B6CB9450021D80A /* BiopotDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEA92B0414FD00022A61 /* BiopotDevice.swift */; }; + A97A9A372B6CB9450021D80A /* PatientListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */; }; + A97A9A382B6CB9450021D80A /* EEGChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8122AB7B430000C4C2F /* EEGChannel.swift */; }; + A97A9A392B6CB9450021D80A /* EEGReading+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F82AB7B41B000C4C2F /* EEGReading+Muse.swift */; }; + A97A9A3A2B6CB9450021D80A /* EEGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8132AB7B430000C4C2F /* EEGReading.swift */; }; + A97A9A3B2B6CB9450021D80A /* PatientSearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */; }; + A97A9A3C2B6CB9450021D80A /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; + A97A9A3D2B6CB9450021D80A /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; + A97A9A3E2B6CB9450021D80A /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; + A97A9A3F2B6CB9450021D80A /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; + A97A9A402B6CB9450021D80A /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; + A97A9A412B6CB9450021D80A /* MuseConnectingProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */; }; + A97A9A422B6CB9450021D80A /* DeviceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */; }; + A97A9A432B6CB9450021D80A /* IXNMusePreset+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */; }; + A97A9A442B6CB9450021D80A /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; + A97A9A452B6CB9450021D80A /* MuseDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */; }; + A97A9A462B6CB9450021D80A /* PatientListModel+QuestionnaireResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */; }; + A97A9A472B6CB9450021D80A /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; + A97A9A482B6CB9450021D80A /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340B2AEAE6380095AAD3 /* TilesView.swift */; }; + A97A9A492B6CB9450021D80A /* AccelerometerSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA382B195D4800FB69FB /* AccelerometerSample.swift */; }; + A97A9A4A2B6CB9450021D80A /* SimpleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */; }; + A97A9A4B2B6CB9450021D80A /* HeadbandFit+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */; }; + A97A9A4C2B6CB9450021D80A /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; + A97A9A4D2B6CB9450021D80A /* ByteBuffer+Int24.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA3E2B1964B500FB69FB /* ByteBuffer+Int24.swift */; }; + A97A9A4E2B6CB9450021D80A /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; + A97A9A4F2B6CB9450021D80A /* MuseHeadbandFitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */; }; + A97A9A502B6CB9450021D80A /* MockDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockDevice.swift */; }; + A97A9A512B6CB9450021D80A /* MockMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */; }; + A97A9A522B6CB9450021D80A /* PatientInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FE89AC206431F90166 /* PatientInformation.swift */; }; + A97A9A532B6CB9450021D80A /* NewPatientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */; }; + A97A9A542B6CB9450021D80A /* CompletedTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */; }; + A97A9A552B6CB9450021D80A /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */; }; + A97A9A562B6CB9450021D80A /* PatientList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientList.swift */; }; + A97A9A572B6CB9450021D80A /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; + A97A9A582B6CB9450021D80A /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; + A97A9A592B6CB9450021D80A /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; + A97A9A5A2B6CB9450021D80A /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; + A97A9A5B2B6CB9450021D80A /* MuseAboutDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */; }; + A97A9A5C2B6CB9450021D80A /* DeviceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */; }; + A97A9A5D2B6CB9450021D80A /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; + A97A9A5E2B6CB9450021D80A /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; + A97A9A5F2B6CB9450021D80A /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; + A97A9A602B6CB9450021D80A /* BiopotDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */; }; + A97A9A612B6CB9450021D80A /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; + A97A9A622B6CB9450021D80A /* EEGRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */; }; + A97A9A632B6CB9450021D80A /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */; }; + A97A9A642B6CB9450021D80A /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; + A97A9A652B6CB9450021D80A /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; + A97A9A662B6CB9450021D80A /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */; }; + A97A9A672B6CB9450021D80A /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; + A97A9A682B6CB9450021D80A /* MuseHeadbandFitProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */; }; + A97A9A692B6CB9450021D80A /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */; }; + A97A9A6B2B6CB9450021D80A /* SpeziPersonalInfo in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */; }; + A97A9A6C2B6CB9450021D80A /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E52B6CB9450021D80A /* SpeziAccount */; }; + A97A9A6D2B6CB9450021D80A /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99FC2B6CB9450021D80A /* BluetoothViews */; }; + A97A9A6E2B6CB9450021D80A /* SpeziBluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F92B6CB9450021D80A /* SpeziBluetooth */; }; + A97A9A6F2B6CB9450021D80A /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E72B6CB9450021D80A /* SpeziContact */; }; + A97A9A712B6CB9450021D80A /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F12B6CB9450021D80A /* SpeziLocalStorage */; }; + A97A9A722B6CB9450021D80A /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F32B6CB9450021D80A /* SpeziSecureStorage */; }; + A97A9A732B6CB9450021D80A /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F62B6CB9450021D80A /* OrderedCollections */; }; + A97A9A742B6CB9450021D80A /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */; }; + A97A9A752B6CB9450021D80A /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E32B6CB9450021D80A /* Spezi */; }; + A97A9A762B6CB9450021D80A /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99FB2B6CB9450021D80A /* BluetoothServices */; }; + A97A9A772B6CB9450021D80A /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F42B6CB9450021D80A /* SpeziViews */; }; + A97A9A782B6CB9450021D80A /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99ED2B6CB9450021D80A /* SpeziOnboarding */; }; + A97A9A792B6CB9450021D80A /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99EC2B6CB9450021D80A /* SpeziFirestore */; }; + A97A9A7A2B6CB9450021D80A /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */; }; + A97A9A7C2B6CB9450021D80A /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; + A97A9A7D2B6CB9450021D80A /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */; }; + A97A9A7E2B6CB9450021D80A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; + A97A9A7F2B6CB9450021D80A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; + A97A9A802B6CB9450021D80A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; + A97A9A812B6CB9450021D80A /* M_CHAT_R_F-en-US-v1.1.json in Resources */ = {isa = PBXBuildFile; fileRef = A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */; }; A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F232B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */; }; @@ -390,6 +519,8 @@ A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGRecordingSession.swift; sourceTree = ""; }; A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy-Info.plist"; sourceTree = ""; }; + A97A9A872B6CB9450021D80A /* NAMS Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NAMS Vision.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + A97A9A882B6CB9450021D80A /* NAMS copy2-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy2-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy2-Info.plist"; sourceTree = ""; }; A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartRecordingView.swift; sourceTree = ""; }; A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EEGChannel+Biopot.swift"; sourceTree = ""; }; A988FEA92B0414FD00022A61 /* BiopotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDevice.swift; sourceTree = ""; }; @@ -494,6 +625,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A97A9A6A2B6CB9450021D80A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A97A9A6B2B6CB9450021D80A /* SpeziPersonalInfo in Frameworks */, + A97A9A6C2B6CB9450021D80A /* SpeziAccount in Frameworks */, + A97A9A6D2B6CB9450021D80A /* BluetoothViews in Frameworks */, + A97A9A6E2B6CB9450021D80A /* SpeziBluetooth in Frameworks */, + A97A9A6F2B6CB9450021D80A /* SpeziContact in Frameworks */, + A97A9A712B6CB9450021D80A /* SpeziLocalStorage in Frameworks */, + A97A9A722B6CB9450021D80A /* SpeziSecureStorage in Frameworks */, + A97A9A732B6CB9450021D80A /* OrderedCollections in Frameworks */, + A97A9A742B6CB9450021D80A /* SpeziFirebaseAccount in Frameworks */, + A97A9A752B6CB9450021D80A /* Spezi in Frameworks */, + A97A9A762B6CB9450021D80A /* BluetoothServices in Frameworks */, + A97A9A772B6CB9450021D80A /* SpeziViews in Frameworks */, + A97A9A782B6CB9450021D80A /* SpeziOnboarding in Frameworks */, + A97A9A792B6CB9450021D80A /* SpeziFirestore in Frameworks */, + A97A9A7A2B6CB9450021D80A /* SpeziFirebaseConfiguration in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -564,6 +717,7 @@ 653A256A28338800005D4D48 /* NAMSUITests */, 653A254E283387FE005D4D48 /* Products */, A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */, + A97A9A882B6CB9450021D80A /* NAMS copy2-Info.plist */, ); sourceTree = ""; }; @@ -574,6 +728,7 @@ 653A255D28338800005D4D48 /* NAMSTests.xctest */, 653A256728338800005D4D48 /* NAMSUITests.xctest */, A926D7C32AB7A552000C4C2F /* NAMS Muse.app */, + A97A9A872B6CB9450021D80A /* NAMS Vision.app */, ); name = Products; sourceTree = ""; @@ -1015,6 +1170,41 @@ productReference = A926D7C32AB7A552000C4C2F /* NAMS Muse.app */; productType = "com.apple.product-type.application"; }; + A97A99E22B6CB9450021D80A /* NAMS Vision */ = { + isa = PBXNativeTarget; + buildConfigurationList = A97A9A832B6CB9450021D80A /* Build configuration list for PBXNativeTarget "NAMS Vision" */; + buildPhases = ( + A97A99FD2B6CB9450021D80A /* Sources */, + A97A9A6A2B6CB9450021D80A /* Frameworks */, + A97A9A7B2B6CB9450021D80A /* Resources */, + A97A9A822B6CB9450021D80A /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "NAMS Vision"; + packageProductDependencies = ( + A97A99E32B6CB9450021D80A /* Spezi */, + A97A99E52B6CB9450021D80A /* SpeziAccount */, + A97A99E72B6CB9450021D80A /* SpeziContact */, + A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */, + A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */, + A97A99EC2B6CB9450021D80A /* SpeziFirestore */, + A97A99ED2B6CB9450021D80A /* SpeziOnboarding */, + A97A99F12B6CB9450021D80A /* SpeziLocalStorage */, + A97A99F32B6CB9450021D80A /* SpeziSecureStorage */, + A97A99F42B6CB9450021D80A /* SpeziViews */, + A97A99F62B6CB9450021D80A /* OrderedCollections */, + A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */, + A97A99F92B6CB9450021D80A /* SpeziBluetooth */, + A97A99FB2B6CB9450021D80A /* BluetoothServices */, + A97A99FC2B6CB9450021D80A /* BluetoothViews */, + ); + productName = NAMS; + productReference = A97A9A872B6CB9450021D80A /* NAMS Vision.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1068,6 +1258,7 @@ targets = ( 653A254C283387FE005D4D48 /* NAMS */, A926D7642AB7A552000C4C2F /* NAMS Muse */, + A97A99E22B6CB9450021D80A /* NAMS Vision */, 653A255C28338800005D4D48 /* NAMSTests */, 653A256628338800005D4D48 /* NAMSUITests */, ); @@ -1115,6 +1306,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A97A9A7B2B6CB9450021D80A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A97A9A7C2B6CB9450021D80A /* ConsentDocument.md in Resources */, + A97A9A7D2B6CB9450021D80A /* AppIcon.png in Resources */, + A97A9A7E2B6CB9450021D80A /* Assets.xcassets in Resources */, + A97A9A7F2B6CB9450021D80A /* Localizable.xcstrings in Resources */, + A97A9A802B6CB9450021D80A /* GoogleService-Info.plist in Resources */, + A97A9A812B6CB9450021D80A /* M_CHAT_R_F-en-US-v1.1.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1154,6 +1358,24 @@ shellPath = /bin/sh; shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; }; + A97A9A822B6CB9450021D80A /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1409,6 +1631,121 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A97A99FD2B6CB9450021D80A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A97A99FE2B6CB9450021D80A /* EEGSample.swift in Sources */, + A97A99FF2B6CB9450021D80A /* Questionnaire+NAMS.swift in Sources */, + A97A9A002B6CB9450021D80A /* ImpedanceMeasurement.swift in Sources */, + A97A9A012B6CB9450021D80A /* AccountSheet.swift in Sources */, + A97A9A022B6CB9450021D80A /* IXNMuseModel+Description.swift in Sources */, + A97A9A032B6CB9450021D80A /* StorageKeys.swift in Sources */, + A97A9A042B6CB9450021D80A /* SearchToken.swift in Sources */, + A97A9A052B6CB9450021D80A /* AddPatientView.swift in Sources */, + A97A9A062B6CB9450021D80A /* ScreeningTask.swift in Sources */, + A97A9A072B6CB9450021D80A /* EEGChannel+Muse.swift in Sources */, + A97A9A082B6CB9450021D80A /* EEGRecording.swift in Sources */, + A97A9A092B6CB9450021D80A /* NotificationPermissions.swift in Sources */, + A97A9A0A2B6CB9450021D80A /* DataControl.swift in Sources */, + A97A9A0B2B6CB9450021D80A /* ScreeningTile.swift in Sources */, + A97A9A0C2B6CB9450021D80A /* CurrentPatientLabel.swift in Sources */, + A97A9A0D2B6CB9450021D80A /* Welcome.swift in Sources */, + A97A9A0E2B6CB9450021D80A /* MuseInterventionRequiredHint.swift in Sources */, + A97A9A0F2B6CB9450021D80A /* ScheduleView.swift in Sources */, + A97A9A102B6CB9450021D80A /* Binding+Negate.swift in Sources */, + A97A9A112B6CB9450021D80A /* ProcessInfo+PreviewSimulator.swift in Sources */, + A97A9A122B6CB9450021D80A /* AccountSetupHeader.swift in Sources */, + A97A9A132B6CB9450021D80A /* EEGChart.swift in Sources */, + A97A9A142B6CB9450021D80A /* MeasurementTile.swift in Sources */, + A97A9A152B6CB9450021D80A /* SelectedPatientCard.swift in Sources */, + A97A9A162B6CB9450021D80A /* EEGChannelMark.swift in Sources */, + A97A9A172B6CB9450021D80A /* OnboardingFlow+PreviewSimulator.swift in Sources */, + A97A9A182B6CB9450021D80A /* PatientListSheet.swift in Sources */, + A97A9A192B6CB9450021D80A /* Home.swift in Sources */, + A97A9A1A2B6CB9450021D80A /* AccountButton.swift in Sources */, + A97A9A1B2B6CB9450021D80A /* MuseBatteryDetailsSection.swift in Sources */, + A97A9A1C2B6CB9450021D80A /* EEGSeries.swift in Sources */, + A97A9A1D2B6CB9450021D80A /* EEGChannel+Biopot.swift in Sources */, + A97A9A1E2B6CB9450021D80A /* MuseHeadbandFitSection.swift in Sources */, + A97A9A1F2B6CB9450021D80A /* OnboardingFlow.swift in Sources */, + A97A9A202B6CB9450021D80A /* MuseBatteryProblemsHint.swift in Sources */, + A97A9A212B6CB9450021D80A /* AccountOnboarding.swift in Sources */, + A97A9A222B6CB9450021D80A /* CodableArray+RawRepresentable.swift in Sources */, + A97A9A232B6CB9450021D80A /* FeatureFlags.swift in Sources */, + A97A9A242B6CB9450021D80A /* DataAcquisition.swift in Sources */, + A97A9A252B6CB9450021D80A /* ScreeningTileHeader.swift in Sources */, + A97A9A262B6CB9450021D80A /* Bundle+Image.swift in Sources */, + A97A9A272B6CB9450021D80A /* DeviceInformation.swift in Sources */, + A97A9A282B6CB9450021D80A /* MeasurementTask.swift in Sources */, + A97A9A292B6CB9450021D80A /* CollectionReference+AsyncAwait.swift in Sources */, + A97A9A2A2B6CB9450021D80A /* EEGSeries+Muse.swift in Sources */, + A97A9A2B2B6CB9450021D80A /* NAMSTestingSetup.swift in Sources */, + A97A9A2C2B6CB9450021D80A /* SamplingConfiguration.swift in Sources */, + A97A9A2D2B6CB9450021D80A /* NAMSAppDelegate.swift in Sources */, + A97A9A2E2B6CB9450021D80A /* QuestionnaireError.swift in Sources */, + A97A9A2F2B6CB9450021D80A /* NAMSApp.swift in Sources */, + A97A9A302B6CB9450021D80A /* PatientRow.swift in Sources */, + A97A9A312B6CB9450021D80A /* Patient.swift in Sources */, + A97A9A322B6CB9450021D80A /* Contacts.swift in Sources */, + A97A9A332B6CB9450021D80A /* CompletedTask.swift in Sources */, + A97A9A342B6CB9450021D80A /* BatteryIcon.swift in Sources */, + A97A9A352B6CB9450021D80A /* FinishedSetup.swift in Sources */, + A97A9A362B6CB9450021D80A /* BiopotDevice.swift in Sources */, + A97A9A372B6CB9450021D80A /* PatientListModel.swift in Sources */, + A97A9A382B6CB9450021D80A /* EEGChannel.swift in Sources */, + A97A9A392B6CB9450021D80A /* EEGReading+Muse.swift in Sources */, + A97A9A3A2B6CB9450021D80A /* EEGReading.swift in Sources */, + A97A9A3B2B6CB9450021D80A /* PatientSearchModel.swift in Sources */, + A97A9A3C2B6CB9450021D80A /* ConnectionState+Muse.swift in Sources */, + A97A9A3D2B6CB9450021D80A /* MockDeviceManager.swift in Sources */, + A97A9A3E2B6CB9450021D80A /* EEGRecordingSession.swift in Sources */, + A97A9A3F2B6CB9450021D80A /* IXNMuseVersion+String.swift in Sources */, + A97A9A402B6CB9450021D80A /* IXNMuseConfiguration+Description.swift in Sources */, + A97A9A412B6CB9450021D80A /* MuseConnectingProblemsHint.swift in Sources */, + A97A9A422B6CB9450021D80A /* DeviceConfiguration.swift in Sources */, + A97A9A432B6CB9450021D80A /* IXNMusePreset+Description.swift in Sources */, + A97A9A442B6CB9450021D80A /* EEGFrequency.swift in Sources */, + A97A9A452B6CB9450021D80A /* MuseDeviceManager.swift in Sources */, + A97A9A462B6CB9450021D80A /* PatientListModel+QuestionnaireResponse.swift in Sources */, + A97A9A472B6CB9450021D80A /* IXNMuseDataPacketType+Type.swift in Sources */, + A97A9A482B6CB9450021D80A /* TilesView.swift in Sources */, + A97A9A492B6CB9450021D80A /* AccelerometerSample.swift in Sources */, + A97A9A4A2B6CB9450021D80A /* SimpleTile.swift in Sources */, + A97A9A4B2B6CB9450021D80A /* HeadbandFit+Muse.swift in Sources */, + A97A9A4C2B6CB9450021D80A /* StartRecordingView.swift in Sources */, + A97A9A4D2B6CB9450021D80A /* ByteBuffer+Int24.swift in Sources */, + A97A9A4E2B6CB9450021D80A /* NearbyDevicesView.swift in Sources */, + A97A9A4F2B6CB9450021D80A /* MuseHeadbandFitView.swift in Sources */, + A97A9A502B6CB9450021D80A /* MockDevice.swift in Sources */, + A97A9A512B6CB9450021D80A /* MockMeasurementGenerator.swift in Sources */, + A97A9A522B6CB9450021D80A /* PatientInformation.swift in Sources */, + A97A9A532B6CB9450021D80A /* NewPatientModel.swift in Sources */, + A97A9A542B6CB9450021D80A /* CompletedTile.swift in Sources */, + A97A9A552B6CB9450021D80A /* MuseDeviceDetailsView.swift in Sources */, + A97A9A562B6CB9450021D80A /* PatientList.swift in Sources */, + A97A9A572B6CB9450021D80A /* TileType.swift in Sources */, + A97A9A582B6CB9450021D80A /* PatientTask.swift in Sources */, + A97A9A592B6CB9450021D80A /* BiopotDevicePreview.swift in Sources */, + A97A9A5A2B6CB9450021D80A /* MuseDevice.swift in Sources */, + A97A9A5B2B6CB9450021D80A /* MuseAboutDetailsSection.swift in Sources */, + A97A9A5C2B6CB9450021D80A /* DeviceCoordinator.swift in Sources */, + A97A9A5D2B6CB9450021D80A /* MuseDeviceRow.swift in Sources */, + A97A9A5E2B6CB9450021D80A /* MuseDeviceList.swift in Sources */, + A97A9A5F2B6CB9450021D80A /* MockDeviceRow.swift in Sources */, + A97A9A602B6CB9450021D80A /* BiopotDeviceRow.swift in Sources */, + A97A9A612B6CB9450021D80A /* BiopotDeviceDetailsView.swift in Sources */, + A97A9A622B6CB9450021D80A /* EEGRecordings.swift in Sources */, + A97A9A632B6CB9450021D80A /* HeadbandFit.swift in Sources */, + A97A9A642B6CB9450021D80A /* ConnectionState.swift in Sources */, + A97A9A652B6CB9450021D80A /* FitLabel.swift in Sources */, + A97A9A662B6CB9450021D80A /* Fit.swift in Sources */, + A97A9A672B6CB9450021D80A /* Questionnaire+Identifiable.swift in Sources */, + A97A9A682B6CB9450021D80A /* MuseHeadbandFitProblemsHint.swift in Sources */, + A97A9A692B6CB9450021D80A /* ConnectedDevice.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1862,6 +2199,185 @@ }; name = Release; }; + A97A9A842B6CB9450021D80A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "NAMS copy2-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = NAMS; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; + INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG VISION"; + "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Debug; + }; + A97A9A852B6CB9450021D80A /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "NAMS copy2-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = NAMS; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; + INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST VISION"; + "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Test; + }; + A97A9A862B6CB9450021D80A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "NAMS copy2-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = NAMS; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; + INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = VISION; + "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; A9EC7BFA2AE715F9003D572E /* Test */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2136,6 +2652,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A97A9A832B6CB9450021D80A /* Build configuration list for PBXNativeTarget "NAMS Vision" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A97A9A842B6CB9450021D80A /* Debug */, + A97A9A852B6CB9450021D80A /* Test */, + A97A9A862B6CB9450021D80A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -2175,8 +2701,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; + branch = "feature/vision-os"; + kind = branch; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { @@ -2199,8 +2725,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.1.1; + branch = "feature/platform-support"; + kind = branch; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { @@ -2291,6 +2817,78 @@ minimumVersion = 1.0.5; }; }; + A97A99E42B6CB9450021D80A /* XCRemoteSwiftPackageReference "Spezi" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/Spezi"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A97A99E62B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziAccount" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; + A97A99E82B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziContact" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A97A99EE2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.1; + }; + }; + A97A99F72B6CB9450021D80A /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.5; + }; + }; + A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; + requirement = { + branch = "feature/unit-testing-setup"; + kind = branch; + }; + }; A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; @@ -2437,6 +3035,81 @@ package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; productName = BluetoothViews; }; + A97A99E32B6CB9450021D80A /* Spezi */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99E42B6CB9450021D80A /* XCRemoteSwiftPackageReference "Spezi" */; + productName = Spezi; + }; + A97A99E52B6CB9450021D80A /* SpeziAccount */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99E62B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziAccount" */; + productName = SpeziAccount; + }; + A97A99E72B6CB9450021D80A /* SpeziContact */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99E82B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziContact" */; + productName = SpeziContact; + }; + A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseAccount; + }; + A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirebaseConfiguration; + }; + A97A99EC2B6CB9450021D80A /* SpeziFirestore */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; + productName = SpeziFirestore; + }; + A97A99ED2B6CB9450021D80A /* SpeziOnboarding */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99EE2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziOnboarding" */; + productName = SpeziOnboarding; + }; + A97A99F12B6CB9450021D80A /* SpeziLocalStorage */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */; + productName = SpeziLocalStorage; + }; + A97A99F32B6CB9450021D80A /* SpeziSecureStorage */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */; + productName = SpeziSecureStorage; + }; + A97A99F42B6CB9450021D80A /* SpeziViews */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */; + productName = SpeziViews; + }; + A97A99F62B6CB9450021D80A /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99F72B6CB9450021D80A /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; + A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */; + productName = SpeziPersonalInfo; + }; + A97A99F92B6CB9450021D80A /* SpeziBluetooth */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = SpeziBluetooth; + }; + A97A99FB2B6CB9450021D80A /* BluetoothServices */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothServices; + }; + A97A99FC2B6CB9450021D80A /* BluetoothViews */ = { + isa = XCSwiftPackageProductDependency; + package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = BluetoothViews; + }; A988FEA52B03FB4A00022A61 /* SpeziBluetooth */ = { isa = XCSwiftPackageProductDependency; package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8165135..8329bd2 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,12 @@ { "pins" : [ { - "identity" : "abseil-cpp-binary", + "identity" : "abseil-cpp-swiftpm", "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", + "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", "state" : { - "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", - "version" : "1.2022062300.0" + "revision" : "06e7506b74bfc47c70f3353e2927ea3e2275230c", + "version" : "0.20220623.1" } }, { @@ -18,6 +18,15 @@ "version" : "10.18.1" } }, + { + "identity" : "boringssl-swiftpm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/boringssl-SwiftPM.git", + "state" : { + "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", + "version" : "0.9.1" + } + }, { "identity" : "fhirmodels", "kind" : "remoteSourceControl", @@ -64,12 +73,12 @@ } }, { - "identity" : "grpc-binary", + "identity" : "grpc-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", + "location" : "https://github.com/grpc/grpc-ios.git", "state" : { - "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", - "version" : "1.49.1" + "revision" : "da8a2405e9fdf3dfb127cefc5af56c29be8df2f2", + "version" : "1.49.2" } }, { @@ -194,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding.git", "state" : { - "revision" : "8fb6d9f1a080661c0cc564a93b82ead3c8d44d4f", - "version" : "1.0.2" + "branch" : "feature/vision-os", + "revision" : "a11d24e8917c11c1bc226eb970a7419047a035f7" } }, { @@ -221,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "138d3326ca348761ebb3fe5d275dd5c89a1cc51d", - "version" : "1.1.1" + "branch" : "feature/platform-support", + "revision" : "a469d40b0a02dffd77807c50db89c54f6af629e7" } }, { diff --git a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Vision.xcscheme b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Vision.xcscheme new file mode 100644 index 0000000..df35015 --- /dev/null +++ b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Vision.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 2e9b4f5..879d418 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -36,7 +36,7 @@ struct HomeView: View { // TODO: how to toggle mock device manager? - @State var mockDeviceManager: MockDeviceManager? // TODO: = MockDeviceManager() + @State var mockDeviceManager = MockDeviceManager() // TODO: = MockDeviceManager() #if MUSE @State var museDeviceManager = MuseDeviceManager() #endif @@ -91,6 +91,7 @@ struct HomeView: View { guard let biopot else { return } + // TODO: we kinda also need connecting state! // a new device is connected now // TODO: remove diff --git a/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift index 5a7b332..31655c4 100644 --- a/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift +++ b/NAMS/Patients/Model/PatientListModel+QuestionnaireResponse.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +#if !VISION import SpeziFirestore import SpeziQuestionnaire @@ -28,3 +29,4 @@ extension PatientListModel { try await add(task: task) } } +#endif diff --git a/NAMS/Patients/Tasks/CompletedTask.swift b/NAMS/Patients/Tasks/CompletedTask.swift index f1570cd..92df867 100644 --- a/NAMS/Patients/Tasks/CompletedTask.swift +++ b/NAMS/Patients/Tasks/CompletedTask.swift @@ -7,11 +7,15 @@ // import FirebaseFirestoreSwift +#if !VISION import SpeziQuestionnaire +#endif enum TaskContent { + #if !VISION case questionnaireResponse(_ response: QuestionnaireResponse) + #endif case eegRecording // currently empty } @@ -23,7 +27,7 @@ struct CompletedTask: Codable { } -extension TaskContent: Codable { +extension TaskContent: Codable { // TODO: this codable conformance breaks with visionOs? is that a big deal? private enum CodingKeys: String, CodingKey { case type case questionnaireResponse @@ -42,9 +46,11 @@ extension TaskContent: Codable { let type = try container.decode(String.self, forKey: .type) switch type { + #if !VISION case Self.questionnaireResponseType: let response = try container.decode(QuestionnaireResponse.self, forKey: .questionnaireResponse) self = .questionnaireResponse(response) + #endif case Self.eegRecordingType: self = .eegRecording default: @@ -56,9 +62,11 @@ extension TaskContent: Codable { var container = encoder.container(keyedBy: CodingKeys.self) switch self { + #if !VISION case let .questionnaireResponse(response): try container.encode(Self.questionnaireResponseType, forKey: .type) try container.encode(response, forKey: .questionnaireResponse) + #endif case .eegRecording: try container.encode(Self.eegRecordingType, forKey: .type) // nothing to encode yet diff --git a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift index 901cb4a..1f129d0 100644 --- a/NAMS/Patients/Tasks/Questionnaire+NAMS.swift +++ b/NAMS/Patients/Tasks/Questionnaire+NAMS.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +#if !VISION import Foundation import SpeziQuestionnaire @@ -31,3 +32,4 @@ extension Questionnaire { questionnaire(withName: "M_CHAT_R_F-en-US-v1.1", bundle: .main) }() } +#endif diff --git a/NAMS/Patients/Tasks/ScreeningTask.swift b/NAMS/Patients/Tasks/ScreeningTask.swift index f5082c9..1ac702a 100644 --- a/NAMS/Patients/Tasks/ScreeningTask.swift +++ b/NAMS/Patients/Tasks/ScreeningTask.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +#if !VISION import Foundation import SpeziQuestionnaire @@ -49,3 +50,4 @@ extension ScreeningTask { ) }() } +#endif diff --git a/NAMS/Tiles/ScreeningTile.swift b/NAMS/Tiles/ScreeningTile.swift index c7b3c50..ced5a9e 100644 --- a/NAMS/Tiles/ScreeningTile.swift +++ b/NAMS/Tiles/ScreeningTile.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +#if !VISION import SpeziQuestionnaire import SwiftUI @@ -83,3 +84,5 @@ struct ScreeningTile: View { } } #endif + +#endif diff --git a/NAMS/Tiles/ScreeningTileHeader.swift b/NAMS/Tiles/ScreeningTileHeader.swift index 4ddcc86..22d925c 100644 --- a/NAMS/Tiles/ScreeningTileHeader.swift +++ b/NAMS/Tiles/ScreeningTileHeader.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +#if !VISION import SpeziViews import SwiftUI @@ -85,3 +86,4 @@ struct ScreeningTileHeader: View { } } #endif +#endif diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index effbc70..6857a48 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -8,7 +8,9 @@ import Spezi import SpeziBluetooth +#if !VISION import SpeziQuestionnaire +#endif import SpeziViews import SwiftUI @@ -23,12 +25,15 @@ struct TilesView: View { @State private var viewState: ViewState = .idle - @State private var presentingQuestionnaire: Questionnaire? @State private var presentingEEGRecording = false + #if !VISION + @State private var presentingQuestionnaire: Questionnaire? + private var questionnaires: [ScreeningTask] { taskList(ScreeningTask.all) } + #endif private var measurements: [MeasurementTask] { taskList(MeasurementTask.all) @@ -49,13 +54,16 @@ struct TilesView: View { } } + #if !VISION Section("Screening") { ForEach(questionnaires) { questionnaire in ScreeningTile(task: questionnaire, presentingItem: $presentingQuestionnaire) } } + #endif } .viewStateAlert(state: $viewState) + #if !VISION .sheet(item: $presentingQuestionnaire) { questionnaire in QuestionnaireView(questionnaire: questionnaire) { result in presentingQuestionnaire = nil @@ -72,6 +80,7 @@ struct TilesView: View { } .interactiveDismissDisabled() } + #endif .sheet(isPresented: $presentingEEGRecording) { NavigationStack { EEGRecording() diff --git a/NAMS/Utils/Questionnaire+Identifiable.swift b/NAMS/Utils/Questionnaire+Identifiable.swift index 689f873..7cd056a 100644 --- a/NAMS/Utils/Questionnaire+Identifiable.swift +++ b/NAMS/Utils/Questionnaire+Identifiable.swift @@ -6,7 +6,9 @@ // SPDX-License-Identifier: MIT // +#if !VISION import SpeziQuestionnaire extension Questionnaire: Identifiable {} +#endif From 3ebee9f2dbd308f82d032647f90016c1385e4473 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 2 Feb 2024 16:58:32 -0800 Subject: [PATCH 09/21] Restructure targets --- NAMS copy2-Info.plist | 19 - NAMS.xcodeproj/project.pbxproj | 710 +----------------- .../xcshareddata/xcschemes/NAMS Muse.xcscheme | 1 + .../xcshareddata/xcschemes/NAMS.xcscheme | 1 + ...tientListModel+QuestionnaireResponse.swift | 2 +- NAMS/Patients/Tasks/CompletedTask.swift | 8 +- NAMS/Patients/Tasks/Questionnaire+NAMS.swift | 2 +- NAMS/Patients/Tasks/ScreeningTask.swift | 2 +- NAMS/Tiles/ScreeningTile.swift | 2 +- NAMS/Tiles/ScreeningTileHeader.swift | 2 +- NAMS/Tiles/TilesView.swift | 8 +- NAMS/Utils/Questionnaire+Identifiable.swift | 2 +- 12 files changed, 39 insertions(+), 720 deletions(-) delete mode 100644 NAMS copy2-Info.plist diff --git a/NAMS copy2-Info.plist b/NAMS copy2-Info.plist deleted file mode 100644 index 1b53361..0000000 --- a/NAMS copy2-Info.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - ITSAppUsesNonExemptEncryption - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - - UISupportedExternalAccessoryProtocols - - com.interaxon.muse - - - diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index bc853b9..c489e77 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -89,7 +89,7 @@ 2FE5DC7729EDD8E6004B9AB4 /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7629EDD8E6004B9AB4 /* SpeziFirebaseConfiguration */; }; 2FE5DC7929EDD8E6004B9AB4 /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC7829EDD8E6004B9AB4 /* SpeziFirestore */; }; 2FE5DC8129EDD91D004B9AB4 /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8029EDD91D004B9AB4 /* SpeziOnboarding */; }; - 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8329EDD934004B9AB4 /* SpeziQuestionnaire */; }; + 2FE5DC8429EDD934004B9AB4 /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = 2FE5DC8329EDD934004B9AB4 /* SpeziQuestionnaire */; }; 2FE5DC8A29EDD972004B9AB4 /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8929EDD972004B9AB4 /* SpeziLocalStorage */; }; 2FE5DC8C29EDD972004B9AB4 /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8B29EDD972004B9AB4 /* SpeziSecureStorage */; }; 2FE5DC8F29EDD980004B9AB4 /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = 2FE5DC8E29EDD980004B9AB4 /* SpeziViews */; }; @@ -132,7 +132,7 @@ A926D7A82AB7A552000C4C2F /* Muse.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A99522432AA61DA6009272F4 /* Muse.framework */; }; A926D7A92AB7A552000C4C2F /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7672AB7A552000C4C2F /* SpeziAccount */; }; A926D7AA2AB7A552000C4C2F /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7692AB7A552000C4C2F /* SpeziContact */; }; - A926D7AB2AB7A552000C4C2F /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7732AB7A552000C4C2F /* SpeziQuestionnaire */; }; + A926D7AB2AB7A552000C4C2F /* SpeziQuestionnaire in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = A926D7732AB7A552000C4C2F /* SpeziQuestionnaire */; }; A926D7AC2AB7A552000C4C2F /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7772AB7A552000C4C2F /* SpeziLocalStorage */; }; A926D7AD2AB7A552000C4C2F /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A926D7792AB7A552000C4C2F /* SpeziSecureStorage */; }; A926D7AE2AB7A552000C4C2F /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A926D76D2AB7A552000C4C2F /* SpeziFirebaseAccount */; }; @@ -198,135 +198,6 @@ A95EEAE52B6B54DA009B4CF8 /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A95EEAE42B6B54DA009B4CF8 /* BluetoothViews */; }; A967061C2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; A967061D2B1AA2E000C17BE5 /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; - A97A99FE2B6CB9450021D80A /* EEGSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA3B2B195ED800FB69FB /* EEGSample.swift */; }; - A97A99FF2B6CB9450021D80A /* Questionnaire+NAMS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FF2AEAE1110095AAD3 /* Questionnaire+NAMS.swift */; }; - A97A9A002B6CB9450021D80A /* ImpedanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C82F912B608756004703E0 /* ImpedanceMeasurement.swift */; }; - A97A9A012B6CB9450021D80A /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42A92AE9EBE200A3F9E5 /* AccountSheet.swift */; }; - A97A9A022B6CB9450021D80A /* IXNMuseModel+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F52AB7B41B000C4C2F /* IXNMuseModel+Description.swift */; }; - A97A9A032B6CB9450021D80A /* StorageKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3F29EDD7EE004B9AB4 /* StorageKeys.swift */; }; - A97A9A042B6CB9450021D80A /* SearchToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5812AE8307800DA8588 /* SearchToken.swift */; }; - A97A9A052B6CB9450021D80A /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; - A97A9A062B6CB9450021D80A /* ScreeningTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */; }; - A97A9A072B6CB9450021D80A /* EEGChannel+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7FA2AB7B41B000C4C2F /* EEGChannel+Muse.swift */; }; - A97A9A082B6CB9450021D80A /* EEGRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80F2AB7B430000C4C2F /* EEGRecording.swift */; }; - A97A9A092B6CB9450021D80A /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD42AB60227006960DF /* NotificationPermissions.swift */; }; - A97A9A0A2B6CB9450021D80A /* DataControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA2F2B192FD500FB69FB /* DataControl.swift */; }; - A97A9A0B2B6CB9450021D80A /* ScreeningTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */; }; - A97A9A0C2B6CB9450021D80A /* CurrentPatientLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB58B2AE84E6D00DA8588 /* CurrentPatientLabel.swift */; }; - A97A9A0D2B6CB9450021D80A /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3429EDD7CA004B9AB4 /* Welcome.swift */; }; - A97A9A0E2B6CB9450021D80A /* MuseInterventionRequiredHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D12B685D800054E27C /* MuseInterventionRequiredHint.swift */; }; - A97A9A0F2B6CB9450021D80A /* ScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534082AEAE3490095AAD3 /* ScheduleView.swift */; }; - A97A9A102B6CB9450021D80A /* Binding+Negate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4229EDD7F2004B9AB4 /* Binding+Negate.swift */; }; - A97A9A112B6CB9450021D80A /* ProcessInfo+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD62AB62012006960DF /* ProcessInfo+PreviewSimulator.swift */; }; - A97A9A122B6CB9450021D80A /* AccountSetupHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */; }; - A97A9A132B6CB9450021D80A /* EEGChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8142AB7B430000C4C2F /* EEGChart.swift */; }; - A97A9A142B6CB9450021D80A /* MeasurementTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */; }; - A97A9A152B6CB9450021D80A /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; - A97A9A162B6CB9450021D80A /* EEGChannelMark.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */; }; - A97A9A172B6CB9450021D80A /* OnboardingFlow+PreviewSimulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A916ADD82AB6217D006960DF /* OnboardingFlow+PreviewSimulator.swift */; }; - A97A9A182B6CB9450021D80A /* PatientListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112C2A36687B00E66E3A /* PatientListSheet.swift */; }; - A97A9A192B6CB9450021D80A /* Home.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC975A72978F11A00BA99FE /* Home.swift */; }; - A97A9A1A2B6CB9450021D80A /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42AB2AE9EBE200A3F9E5 /* AccountButton.swift */; }; - A97A9A1B2B6CB9450021D80A /* MuseBatteryDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CE2B685D800054E27C /* MuseBatteryDetailsSection.swift */; }; - A97A9A1C2B6CB9450021D80A /* EEGSeries.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8112AB7B430000C4C2F /* EEGSeries.swift */; }; - A97A9A1D2B6CB9450021D80A /* EEGChannel+Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */; }; - A97A9A1E2B6CB9450021D80A /* MuseHeadbandFitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CD2B685D800054E27C /* MuseHeadbandFitSection.swift */; }; - A97A9A1F2B6CB9450021D80A /* OnboardingFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3129EDD7CA004B9AB4 /* OnboardingFlow.swift */; }; - A97A9A202B6CB9450021D80A /* MuseBatteryProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D42B685D800054E27C /* MuseBatteryProblemsHint.swift */; }; - A97A9A212B6CB9450021D80A /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */; }; - A97A9A222B6CB9450021D80A /* CodableArray+RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */; }; - A97A9A232B6CB9450021D80A /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC3E29EDD7ED004B9AB4 /* FeatureFlags.swift */; }; - A97A9A242B6CB9450021D80A /* DataAcquisition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA352B1942B800FB69FB /* DataAcquisition.swift */; }; - A97A9A252B6CB9450021D80A /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; - A97A9A262B6CB9450021D80A /* Bundle+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC4329EDD7F2004B9AB4 /* Bundle+Image.swift */; }; - A97A9A272B6CB9450021D80A /* DeviceInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB42B0453E100022A61 /* DeviceInformation.swift */; }; - A97A9A282B6CB9450021D80A /* MeasurementTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCF2AEC5EF50057C7DD /* MeasurementTask.swift */; }; - A97A9A292B6CB9450021D80A /* CollectionReference+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5852AE8347E00DA8588 /* CollectionReference+AsyncAwait.swift */; }; - A97A9A2A2B6CB9450021D80A /* EEGSeries+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F92AB7B41B000C4C2F /* EEGSeries+Muse.swift */; }; - A97A9A2B2B6CB9450021D80A /* NAMSTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* NAMSTestingSetup.swift */; }; - A97A9A2C2B6CB9450021D80A /* SamplingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA322B193C9700FB69FB /* SamplingConfiguration.swift */; }; - A97A9A2D2B6CB9450021D80A /* NAMSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5E32BC297E05EA003432F8 /* NAMSAppDelegate.swift */; }; - A97A9A2E2B6CB9450021D80A /* QuestionnaireError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534172AEB0DB20095AAD3 /* QuestionnaireError.swift */; }; - A97A9A2F2B6CB9450021D80A /* NAMSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* NAMSApp.swift */; }; - A97A9A302B6CB9450021D80A /* PatientRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A989112E2A36688A00E66E3A /* PatientRow.swift */; }; - A97A9A312B6CB9450021D80A /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; - A97A9A322B6CB9450021D80A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2529EDD38A004B9AB4 /* Contacts.swift */; }; - A97A9A332B6CB9450021D80A /* CompletedTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340F2AEAF2AE0095AAD3 /* CompletedTask.swift */; }; - A97A9A342B6CB9450021D80A /* BatteryIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEAB2B043AED00022A61 /* BatteryIcon.swift */; }; - A97A9A352B6CB9450021D80A /* FinishedSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC173772F6FD3C05EBFCE52 /* FinishedSetup.swift */; }; - A97A9A362B6CB9450021D80A /* BiopotDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEA92B0414FD00022A61 /* BiopotDevice.swift */; }; - A97A9A372B6CB9450021D80A /* PatientListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB5882AE83F7E00DA8588 /* PatientListModel.swift */; }; - A97A9A382B6CB9450021D80A /* EEGChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8122AB7B430000C4C2F /* EEGChannel.swift */; }; - A97A9A392B6CB9450021D80A /* EEGReading+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D7F82AB7B41B000C4C2F /* EEGReading+Muse.swift */; }; - A97A9A3A2B6CB9450021D80A /* EEGReading.swift in Sources */ = {isa = PBXBuildFile; fileRef = A926D8132AB7B430000C4C2F /* EEGReading.swift */; }; - A97A9A3B2B6CB9450021D80A /* PatientSearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BCB57E2AE82FFC00DA8588 /* PatientSearchModel.swift */; }; - A97A9A3C2B6CB9450021D80A /* ConnectionState+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1775D695F23F9EB05697F /* ConnectionState+Muse.swift */; }; - A97A9A3D2B6CB9450021D80A /* MockDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC175A8D7EDF6EE1B55E859 /* MockDeviceManager.swift */; }; - A97A9A3E2B6CB9450021D80A /* EEGRecordingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */; }; - A97A9A3F2B6CB9450021D80A /* IXNMuseVersion+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17020D7BBBECE54A765C6 /* IXNMuseVersion+String.swift */; }; - A97A9A402B6CB9450021D80A /* IXNMuseConfiguration+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1785634B3460C4FB953C7 /* IXNMuseConfiguration+Description.swift */; }; - A97A9A412B6CB9450021D80A /* MuseConnectingProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D22B685D800054E27C /* MuseConnectingProblemsHint.swift */; }; - A97A9A422B6CB9450021D80A /* DeviceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988FEB12B0452C400022A61 /* DeviceConfiguration.swift */; }; - A97A9A432B6CB9450021D80A /* IXNMusePreset+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1751EE233BB85004ECA90 /* IXNMusePreset+Description.swift */; }; - A97A9A442B6CB9450021D80A /* EEGFrequency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174A86A693CF5B0ADE824 /* EEGFrequency.swift */; }; - A97A9A452B6CB9450021D80A /* MuseDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17DCD9F44F68CF55EC3BE /* MuseDeviceManager.swift */; }; - A97A9A462B6CB9450021D80A /* PatientListModel+QuestionnaireResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94534122AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift */; }; - A97A9A472B6CB9450021D80A /* IXNMuseDataPacketType+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17CDCBE427101B2ACE5FB /* IXNMuseDataPacketType+Type.swift */; }; - A97A9A482B6CB9450021D80A /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A945340B2AEAE6380095AAD3 /* TilesView.swift */; }; - A97A9A492B6CB9450021D80A /* AccelerometerSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA382B195D4800FB69FB /* AccelerometerSample.swift */; }; - A97A9A4A2B6CB9450021D80A /* SimpleTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECCC2AEC58B00057C7DD /* SimpleTile.swift */; }; - A97A9A4B2B6CB9450021D80A /* HeadbandFit+Muse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FEC8B57C8C069AF84F /* HeadbandFit+Muse.swift */; }; - A97A9A4C2B6CB9450021D80A /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; - A97A9A4D2B6CB9450021D80A /* ByteBuffer+Int24.swift in Sources */ = {isa = PBXBuildFile; fileRef = A907DA3E2B1964B500FB69FB /* ByteBuffer+Int24.swift */; }; - A97A9A4E2B6CB9450021D80A /* NearbyDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */; }; - A97A9A4F2B6CB9450021D80A /* MuseHeadbandFitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E52B6860AC0054E27C /* MuseHeadbandFitView.swift */; }; - A97A9A502B6CB9450021D80A /* MockDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D172D8299600ED13D60 /* MockDevice.swift */; }; - A97A9A512B6CB9450021D80A /* MockMeasurementGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17D48217697F558657E69 /* MockMeasurementGenerator.swift */; }; - A97A9A522B6CB9450021D80A /* PatientInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174FE89AC206431F90166 /* PatientInformation.swift */; }; - A97A9A532B6CB9450021D80A /* NewPatientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1739D3D10EFC5B9F67646 /* NewPatientModel.swift */; }; - A97A9A542B6CB9450021D80A /* CompletedTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */; }; - A97A9A552B6CB9450021D80A /* MuseDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CC2B685D800054E27C /* MuseDeviceDetailsView.swift */; }; - A97A9A562B6CB9450021D80A /* PatientList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17A06799FC1470D4DDC0D /* PatientList.swift */; }; - A97A9A572B6CB9450021D80A /* TileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177BFA6C401C2C87FCD5C /* TileType.swift */; }; - A97A9A582B6CB9450021D80A /* PatientTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC170F68232B993528F84FE /* PatientTask.swift */; }; - A97A9A592B6CB9450021D80A /* BiopotDevicePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177B4AFB1C891EDB8152B /* BiopotDevicePreview.swift */; }; - A97A9A5A2B6CB9450021D80A /* MuseDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17C8A120ECD3394366958 /* MuseDevice.swift */; }; - A97A9A5B2B6CB9450021D80A /* MuseAboutDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8CF2B685D800054E27C /* MuseAboutDetailsSection.swift */; }; - A97A9A5C2B6CB9450021D80A /* DeviceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */; }; - A97A9A5D2B6CB9450021D80A /* MuseDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17FBF21126FAA3A35B601 /* MuseDeviceRow.swift */; }; - A97A9A5E2B6CB9450021D80A /* MuseDeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179BED6FAAD175304A875 /* MuseDeviceList.swift */; }; - A97A9A5F2B6CB9450021D80A /* MockDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC1794C7379ACE157EE11C4 /* MockDeviceRow.swift */; }; - A97A9A602B6CB9450021D80A /* BiopotDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17949BAE6AB7C5F3CDBFC /* BiopotDeviceRow.swift */; }; - A97A9A612B6CB9450021D80A /* BiopotDeviceDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171C8F82A592D576A4E33 /* BiopotDeviceDetailsView.swift */; }; - A97A9A622B6CB9450021D80A /* EEGRecordings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */; }; - A97A9A632B6CB9450021D80A /* HeadbandFit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC171992FCBFA6C59936A3E /* HeadbandFit.swift */; }; - A97A9A642B6CB9450021D80A /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC179219FC061D5D0282BF2 /* ConnectionState.swift */; }; - A97A9A652B6CB9450021D80A /* FitLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8E82B6863D60054E27C /* FitLabel.swift */; }; - A97A9A662B6CB9450021D80A /* Fit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC177DBF00CB14CAC4C7E82 /* Fit.swift */; }; - A97A9A672B6CB9450021D80A /* Questionnaire+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC174AABEF6E6015EFA8B4B /* Questionnaire+Identifiable.swift */; }; - A97A9A682B6CB9450021D80A /* MuseHeadbandFitProblemsHint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8D32B685D800054E27C /* MuseHeadbandFitProblemsHint.swift */; }; - A97A9A692B6CB9450021D80A /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */; }; - A97A9A6B2B6CB9450021D80A /* SpeziPersonalInfo in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */; }; - A97A9A6C2B6CB9450021D80A /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E52B6CB9450021D80A /* SpeziAccount */; }; - A97A9A6D2B6CB9450021D80A /* BluetoothViews in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99FC2B6CB9450021D80A /* BluetoothViews */; }; - A97A9A6E2B6CB9450021D80A /* SpeziBluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F92B6CB9450021D80A /* SpeziBluetooth */; }; - A97A9A6F2B6CB9450021D80A /* SpeziContact in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E72B6CB9450021D80A /* SpeziContact */; }; - A97A9A712B6CB9450021D80A /* SpeziLocalStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F12B6CB9450021D80A /* SpeziLocalStorage */; }; - A97A9A722B6CB9450021D80A /* SpeziSecureStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F32B6CB9450021D80A /* SpeziSecureStorage */; }; - A97A9A732B6CB9450021D80A /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F62B6CB9450021D80A /* OrderedCollections */; }; - A97A9A742B6CB9450021D80A /* SpeziFirebaseAccount in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */; }; - A97A9A752B6CB9450021D80A /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99E32B6CB9450021D80A /* Spezi */; }; - A97A9A762B6CB9450021D80A /* BluetoothServices in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99FB2B6CB9450021D80A /* BluetoothServices */; }; - A97A9A772B6CB9450021D80A /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99F42B6CB9450021D80A /* SpeziViews */; }; - A97A9A782B6CB9450021D80A /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99ED2B6CB9450021D80A /* SpeziOnboarding */; }; - A97A9A792B6CB9450021D80A /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99EC2B6CB9450021D80A /* SpeziFirestore */; }; - A97A9A7A2B6CB9450021D80A /* SpeziFirebaseConfiguration in Frameworks */ = {isa = PBXBuildFile; productRef = A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */; }; - A97A9A7C2B6CB9450021D80A /* ConsentDocument.md in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */; }; - A97A9A7D2B6CB9450021D80A /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 2FE5DC2A29EDD78D004B9AB4 /* AppIcon.png */; }; - A97A9A7E2B6CB9450021D80A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; }; - A97A9A7F2B6CB9450021D80A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A9BCB57B2AE7435E00DA8588 /* Localizable.xcstrings */; }; - A97A9A802B6CB9450021D80A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2F6025CA29BBE70F0045459E /* GoogleService-Info.plist */; }; - A97A9A812B6CB9450021D80A /* M_CHAT_R_F-en-US-v1.1.json in Resources */ = {isa = PBXBuildFile; fileRef = A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */; }; A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F202B1EA0D600E25505 /* StartRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */; }; A97E4F232B1EA21800E25505 /* EEGChannel+Biopot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */; }; @@ -519,8 +390,6 @@ A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGRecordingSession.swift; sourceTree = ""; }; A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy-Info.plist"; sourceTree = ""; }; - A97A9A872B6CB9450021D80A /* NAMS Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NAMS Vision.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - A97A9A882B6CB9450021D80A /* NAMS copy2-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy2-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy2-Info.plist"; sourceTree = ""; }; A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartRecordingView.swift; sourceTree = ""; }; A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EEGChannel+Biopot.swift"; sourceTree = ""; }; A988FEA92B0414FD00022A61 /* BiopotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDevice.swift; sourceTree = ""; }; @@ -625,28 +494,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A97A9A6A2B6CB9450021D80A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A97A9A6B2B6CB9450021D80A /* SpeziPersonalInfo in Frameworks */, - A97A9A6C2B6CB9450021D80A /* SpeziAccount in Frameworks */, - A97A9A6D2B6CB9450021D80A /* BluetoothViews in Frameworks */, - A97A9A6E2B6CB9450021D80A /* SpeziBluetooth in Frameworks */, - A97A9A6F2B6CB9450021D80A /* SpeziContact in Frameworks */, - A97A9A712B6CB9450021D80A /* SpeziLocalStorage in Frameworks */, - A97A9A722B6CB9450021D80A /* SpeziSecureStorage in Frameworks */, - A97A9A732B6CB9450021D80A /* OrderedCollections in Frameworks */, - A97A9A742B6CB9450021D80A /* SpeziFirebaseAccount in Frameworks */, - A97A9A752B6CB9450021D80A /* Spezi in Frameworks */, - A97A9A762B6CB9450021D80A /* BluetoothServices in Frameworks */, - A97A9A772B6CB9450021D80A /* SpeziViews in Frameworks */, - A97A9A782B6CB9450021D80A /* SpeziOnboarding in Frameworks */, - A97A9A792B6CB9450021D80A /* SpeziFirestore in Frameworks */, - A97A9A7A2B6CB9450021D80A /* SpeziFirebaseConfiguration in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -717,7 +564,6 @@ 653A256A28338800005D4D48 /* NAMSUITests */, 653A254E283387FE005D4D48 /* Products */, A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */, - A97A9A882B6CB9450021D80A /* NAMS copy2-Info.plist */, ); sourceTree = ""; }; @@ -728,7 +574,6 @@ 653A255D28338800005D4D48 /* NAMSTests.xctest */, 653A256728338800005D4D48 /* NAMSUITests.xctest */, A926D7C32AB7A552000C4C2F /* NAMS Muse.app */, - A97A9A872B6CB9450021D80A /* NAMS Vision.app */, ); name = Products; sourceTree = ""; @@ -1170,41 +1015,6 @@ productReference = A926D7C32AB7A552000C4C2F /* NAMS Muse.app */; productType = "com.apple.product-type.application"; }; - A97A99E22B6CB9450021D80A /* NAMS Vision */ = { - isa = PBXNativeTarget; - buildConfigurationList = A97A9A832B6CB9450021D80A /* Build configuration list for PBXNativeTarget "NAMS Vision" */; - buildPhases = ( - A97A99FD2B6CB9450021D80A /* Sources */, - A97A9A6A2B6CB9450021D80A /* Frameworks */, - A97A9A7B2B6CB9450021D80A /* Resources */, - A97A9A822B6CB9450021D80A /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "NAMS Vision"; - packageProductDependencies = ( - A97A99E32B6CB9450021D80A /* Spezi */, - A97A99E52B6CB9450021D80A /* SpeziAccount */, - A97A99E72B6CB9450021D80A /* SpeziContact */, - A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */, - A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */, - A97A99EC2B6CB9450021D80A /* SpeziFirestore */, - A97A99ED2B6CB9450021D80A /* SpeziOnboarding */, - A97A99F12B6CB9450021D80A /* SpeziLocalStorage */, - A97A99F32B6CB9450021D80A /* SpeziSecureStorage */, - A97A99F42B6CB9450021D80A /* SpeziViews */, - A97A99F62B6CB9450021D80A /* OrderedCollections */, - A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */, - A97A99F92B6CB9450021D80A /* SpeziBluetooth */, - A97A99FB2B6CB9450021D80A /* BluetoothServices */, - A97A99FC2B6CB9450021D80A /* BluetoothViews */, - ); - productName = NAMS; - productReference = A97A9A872B6CB9450021D80A /* NAMS Vision.app */; - productType = "com.apple.product-type.application"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1213,7 +1023,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1340; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1520; TargetAttributes = { 653A254C283387FE005D4D48 = { CreatedOnToolsVersion = 13.4; @@ -1258,7 +1068,6 @@ targets = ( 653A254C283387FE005D4D48 /* NAMS */, A926D7642AB7A552000C4C2F /* NAMS Muse */, - A97A99E22B6CB9450021D80A /* NAMS Vision */, 653A255C28338800005D4D48 /* NAMSTests */, 653A256628338800005D4D48 /* NAMSUITests */, ); @@ -1306,19 +1115,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A97A9A7B2B6CB9450021D80A /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A97A9A7C2B6CB9450021D80A /* ConsentDocument.md in Resources */, - A97A9A7D2B6CB9450021D80A /* AppIcon.png in Resources */, - A97A9A7E2B6CB9450021D80A /* Assets.xcassets in Resources */, - A97A9A7F2B6CB9450021D80A /* Localizable.xcstrings in Resources */, - A97A9A802B6CB9450021D80A /* GoogleService-Info.plist in Resources */, - A97A9A812B6CB9450021D80A /* M_CHAT_R_F-en-US-v1.1.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1358,24 +1154,6 @@ shellPath = /bin/sh; shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; }; - A97A9A822B6CB9450021D80A /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1631,121 +1409,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A97A99FD2B6CB9450021D80A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A97A99FE2B6CB9450021D80A /* EEGSample.swift in Sources */, - A97A99FF2B6CB9450021D80A /* Questionnaire+NAMS.swift in Sources */, - A97A9A002B6CB9450021D80A /* ImpedanceMeasurement.swift in Sources */, - A97A9A012B6CB9450021D80A /* AccountSheet.swift in Sources */, - A97A9A022B6CB9450021D80A /* IXNMuseModel+Description.swift in Sources */, - A97A9A032B6CB9450021D80A /* StorageKeys.swift in Sources */, - A97A9A042B6CB9450021D80A /* SearchToken.swift in Sources */, - A97A9A052B6CB9450021D80A /* AddPatientView.swift in Sources */, - A97A9A062B6CB9450021D80A /* ScreeningTask.swift in Sources */, - A97A9A072B6CB9450021D80A /* EEGChannel+Muse.swift in Sources */, - A97A9A082B6CB9450021D80A /* EEGRecording.swift in Sources */, - A97A9A092B6CB9450021D80A /* NotificationPermissions.swift in Sources */, - A97A9A0A2B6CB9450021D80A /* DataControl.swift in Sources */, - A97A9A0B2B6CB9450021D80A /* ScreeningTile.swift in Sources */, - A97A9A0C2B6CB9450021D80A /* CurrentPatientLabel.swift in Sources */, - A97A9A0D2B6CB9450021D80A /* Welcome.swift in Sources */, - A97A9A0E2B6CB9450021D80A /* MuseInterventionRequiredHint.swift in Sources */, - A97A9A0F2B6CB9450021D80A /* ScheduleView.swift in Sources */, - A97A9A102B6CB9450021D80A /* Binding+Negate.swift in Sources */, - A97A9A112B6CB9450021D80A /* ProcessInfo+PreviewSimulator.swift in Sources */, - A97A9A122B6CB9450021D80A /* AccountSetupHeader.swift in Sources */, - A97A9A132B6CB9450021D80A /* EEGChart.swift in Sources */, - A97A9A142B6CB9450021D80A /* MeasurementTile.swift in Sources */, - A97A9A152B6CB9450021D80A /* SelectedPatientCard.swift in Sources */, - A97A9A162B6CB9450021D80A /* EEGChannelMark.swift in Sources */, - A97A9A172B6CB9450021D80A /* OnboardingFlow+PreviewSimulator.swift in Sources */, - A97A9A182B6CB9450021D80A /* PatientListSheet.swift in Sources */, - A97A9A192B6CB9450021D80A /* Home.swift in Sources */, - A97A9A1A2B6CB9450021D80A /* AccountButton.swift in Sources */, - A97A9A1B2B6CB9450021D80A /* MuseBatteryDetailsSection.swift in Sources */, - A97A9A1C2B6CB9450021D80A /* EEGSeries.swift in Sources */, - A97A9A1D2B6CB9450021D80A /* EEGChannel+Biopot.swift in Sources */, - A97A9A1E2B6CB9450021D80A /* MuseHeadbandFitSection.swift in Sources */, - A97A9A1F2B6CB9450021D80A /* OnboardingFlow.swift in Sources */, - A97A9A202B6CB9450021D80A /* MuseBatteryProblemsHint.swift in Sources */, - A97A9A212B6CB9450021D80A /* AccountOnboarding.swift in Sources */, - A97A9A222B6CB9450021D80A /* CodableArray+RawRepresentable.swift in Sources */, - A97A9A232B6CB9450021D80A /* FeatureFlags.swift in Sources */, - A97A9A242B6CB9450021D80A /* DataAcquisition.swift in Sources */, - A97A9A252B6CB9450021D80A /* ScreeningTileHeader.swift in Sources */, - A97A9A262B6CB9450021D80A /* Bundle+Image.swift in Sources */, - A97A9A272B6CB9450021D80A /* DeviceInformation.swift in Sources */, - A97A9A282B6CB9450021D80A /* MeasurementTask.swift in Sources */, - A97A9A292B6CB9450021D80A /* CollectionReference+AsyncAwait.swift in Sources */, - A97A9A2A2B6CB9450021D80A /* EEGSeries+Muse.swift in Sources */, - A97A9A2B2B6CB9450021D80A /* NAMSTestingSetup.swift in Sources */, - A97A9A2C2B6CB9450021D80A /* SamplingConfiguration.swift in Sources */, - A97A9A2D2B6CB9450021D80A /* NAMSAppDelegate.swift in Sources */, - A97A9A2E2B6CB9450021D80A /* QuestionnaireError.swift in Sources */, - A97A9A2F2B6CB9450021D80A /* NAMSApp.swift in Sources */, - A97A9A302B6CB9450021D80A /* PatientRow.swift in Sources */, - A97A9A312B6CB9450021D80A /* Patient.swift in Sources */, - A97A9A322B6CB9450021D80A /* Contacts.swift in Sources */, - A97A9A332B6CB9450021D80A /* CompletedTask.swift in Sources */, - A97A9A342B6CB9450021D80A /* BatteryIcon.swift in Sources */, - A97A9A352B6CB9450021D80A /* FinishedSetup.swift in Sources */, - A97A9A362B6CB9450021D80A /* BiopotDevice.swift in Sources */, - A97A9A372B6CB9450021D80A /* PatientListModel.swift in Sources */, - A97A9A382B6CB9450021D80A /* EEGChannel.swift in Sources */, - A97A9A392B6CB9450021D80A /* EEGReading+Muse.swift in Sources */, - A97A9A3A2B6CB9450021D80A /* EEGReading.swift in Sources */, - A97A9A3B2B6CB9450021D80A /* PatientSearchModel.swift in Sources */, - A97A9A3C2B6CB9450021D80A /* ConnectionState+Muse.swift in Sources */, - A97A9A3D2B6CB9450021D80A /* MockDeviceManager.swift in Sources */, - A97A9A3E2B6CB9450021D80A /* EEGRecordingSession.swift in Sources */, - A97A9A3F2B6CB9450021D80A /* IXNMuseVersion+String.swift in Sources */, - A97A9A402B6CB9450021D80A /* IXNMuseConfiguration+Description.swift in Sources */, - A97A9A412B6CB9450021D80A /* MuseConnectingProblemsHint.swift in Sources */, - A97A9A422B6CB9450021D80A /* DeviceConfiguration.swift in Sources */, - A97A9A432B6CB9450021D80A /* IXNMusePreset+Description.swift in Sources */, - A97A9A442B6CB9450021D80A /* EEGFrequency.swift in Sources */, - A97A9A452B6CB9450021D80A /* MuseDeviceManager.swift in Sources */, - A97A9A462B6CB9450021D80A /* PatientListModel+QuestionnaireResponse.swift in Sources */, - A97A9A472B6CB9450021D80A /* IXNMuseDataPacketType+Type.swift in Sources */, - A97A9A482B6CB9450021D80A /* TilesView.swift in Sources */, - A97A9A492B6CB9450021D80A /* AccelerometerSample.swift in Sources */, - A97A9A4A2B6CB9450021D80A /* SimpleTile.swift in Sources */, - A97A9A4B2B6CB9450021D80A /* HeadbandFit+Muse.swift in Sources */, - A97A9A4C2B6CB9450021D80A /* StartRecordingView.swift in Sources */, - A97A9A4D2B6CB9450021D80A /* ByteBuffer+Int24.swift in Sources */, - A97A9A4E2B6CB9450021D80A /* NearbyDevicesView.swift in Sources */, - A97A9A4F2B6CB9450021D80A /* MuseHeadbandFitView.swift in Sources */, - A97A9A502B6CB9450021D80A /* MockDevice.swift in Sources */, - A97A9A512B6CB9450021D80A /* MockMeasurementGenerator.swift in Sources */, - A97A9A522B6CB9450021D80A /* PatientInformation.swift in Sources */, - A97A9A532B6CB9450021D80A /* NewPatientModel.swift in Sources */, - A97A9A542B6CB9450021D80A /* CompletedTile.swift in Sources */, - A97A9A552B6CB9450021D80A /* MuseDeviceDetailsView.swift in Sources */, - A97A9A562B6CB9450021D80A /* PatientList.swift in Sources */, - A97A9A572B6CB9450021D80A /* TileType.swift in Sources */, - A97A9A582B6CB9450021D80A /* PatientTask.swift in Sources */, - A97A9A592B6CB9450021D80A /* BiopotDevicePreview.swift in Sources */, - A97A9A5A2B6CB9450021D80A /* MuseDevice.swift in Sources */, - A97A9A5B2B6CB9450021D80A /* MuseAboutDetailsSection.swift in Sources */, - A97A9A5C2B6CB9450021D80A /* DeviceCoordinator.swift in Sources */, - A97A9A5D2B6CB9450021D80A /* MuseDeviceRow.swift in Sources */, - A97A9A5E2B6CB9450021D80A /* MuseDeviceList.swift in Sources */, - A97A9A5F2B6CB9450021D80A /* MockDeviceRow.swift in Sources */, - A97A9A602B6CB9450021D80A /* BiopotDeviceRow.swift in Sources */, - A97A9A612B6CB9450021D80A /* BiopotDeviceDetailsView.swift in Sources */, - A97A9A622B6CB9450021D80A /* EEGRecordings.swift in Sources */, - A97A9A632B6CB9450021D80A /* HeadbandFit.swift in Sources */, - A97A9A642B6CB9450021D80A /* ConnectionState.swift in Sources */, - A97A9A652B6CB9450021D80A /* FitLabel.swift in Sources */, - A97A9A662B6CB9450021D80A /* Fit.swift in Sources */, - A97A9A672B6CB9450021D80A /* Questionnaire+Identifiable.swift in Sources */, - A97A9A682B6CB9450021D80A /* MuseHeadbandFitProblemsHint.swift in Sources */, - A97A9A692B6CB9450021D80A /* ConnectedDevice.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1766,6 +1429,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1828,6 +1492,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -1927,15 +1592,16 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; @@ -1986,14 +1652,15 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; @@ -2126,16 +1793,17 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MUSE"; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; @@ -2187,189 +1855,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = MUSE; - "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; - A97A9A842B6CB9450021D80A /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "NAMS copy2-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = NAMS; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; - INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG VISION"; - "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; - }; - name = Debug; - }; - A97A9A852B6CB9450021D80A /* Test */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "NAMS copy2-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = NAMS; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; - INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST VISION"; - "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; - }; - name = Test; - }; - A97A9A862B6CB9450021D80A /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = "NAMS/Supporting Files/NAMS.entitlements"; - CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; - ENABLE_PREVIEWS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "NAMS copy2-Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = NAMS; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "This application uses Bluetooth to connect to EEG headbands."; - INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The Neurodevelopment Assessment and Monitoring System (NAMS) uses the step count to demonstrate Spezi's integration with HealthKit."; - INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSMotionUsageDescription = "This message should never appear. Please adjust this when you start using motion information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "This message should never appear. Please adjust this when you start using speecg information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = VISION; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = MUSE; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; @@ -2382,6 +1872,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -2487,15 +1978,16 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Test; }; @@ -2547,16 +2039,17 @@ PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST MUSE"; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Test; }; @@ -2652,16 +2145,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A97A9A832B6CB9450021D80A /* Build configuration list for PBXNativeTarget "NAMS Vision" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A97A9A842B6CB9450021D80A /* Debug */, - A97A9A852B6CB9450021D80A /* Test */, - A97A9A862B6CB9450021D80A /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -2817,78 +2300,6 @@ minimumVersion = 1.0.5; }; }; - A97A99E42B6CB9450021D80A /* XCRemoteSwiftPackageReference "Spezi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/Spezi"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - A97A99E62B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziAccount" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziAccount.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.1.0; - }; - }; - A97A99E82B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziContact" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziContact.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziFirebase.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - A97A99EE2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziOnboarding" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziStorage.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.1.1; - }; - }; - A97A99F72B6CB9450021D80A /* XCRemoteSwiftPackageReference "swift-collections" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-collections.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.5; - }; - }; - A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; - requirement = { - branch = "feature/unit-testing-setup"; - kind = branch; - }; - }; A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; @@ -3035,81 +2446,6 @@ package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; productName = BluetoothViews; }; - A97A99E32B6CB9450021D80A /* Spezi */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99E42B6CB9450021D80A /* XCRemoteSwiftPackageReference "Spezi" */; - productName = Spezi; - }; - A97A99E52B6CB9450021D80A /* SpeziAccount */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99E62B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziAccount" */; - productName = SpeziAccount; - }; - A97A99E72B6CB9450021D80A /* SpeziContact */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99E82B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziContact" */; - productName = SpeziContact; - }; - A97A99E92B6CB9450021D80A /* SpeziFirebaseAccount */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; - productName = SpeziFirebaseAccount; - }; - A97A99EB2B6CB9450021D80A /* SpeziFirebaseConfiguration */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; - productName = SpeziFirebaseConfiguration; - }; - A97A99EC2B6CB9450021D80A /* SpeziFirestore */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99EA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziFirebase" */; - productName = SpeziFirestore; - }; - A97A99ED2B6CB9450021D80A /* SpeziOnboarding */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99EE2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziOnboarding" */; - productName = SpeziOnboarding; - }; - A97A99F12B6CB9450021D80A /* SpeziLocalStorage */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */; - productName = SpeziLocalStorage; - }; - A97A99F32B6CB9450021D80A /* SpeziSecureStorage */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99F22B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziStorage" */; - productName = SpeziSecureStorage; - }; - A97A99F42B6CB9450021D80A /* SpeziViews */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */; - productName = SpeziViews; - }; - A97A99F62B6CB9450021D80A /* OrderedCollections */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99F72B6CB9450021D80A /* XCRemoteSwiftPackageReference "swift-collections" */; - productName = OrderedCollections; - }; - A97A99F82B6CB9450021D80A /* SpeziPersonalInfo */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99F52B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziViews" */; - productName = SpeziPersonalInfo; - }; - A97A99F92B6CB9450021D80A /* SpeziBluetooth */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = SpeziBluetooth; - }; - A97A99FB2B6CB9450021D80A /* BluetoothServices */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothServices; - }; - A97A99FC2B6CB9450021D80A /* BluetoothViews */ = { - isa = XCSwiftPackageProductDependency; - package = A97A99FA2B6CB9450021D80A /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; - productName = BluetoothViews; - }; A988FEA52B03FB4A00022A61 /* SpeziBluetooth */ = { isa = XCSwiftPackageProductDependency; package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; diff --git a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme index 63b534e..de9221c 100644 --- a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme +++ b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme @@ -1,5 +1,6 @@ Date: Sat, 3 Feb 2024 20:12:02 -0800 Subject: [PATCH 10/21] Minor adjustments --- NAMS.xcodeproj/project.pbxproj | 22 +++--- .../xcshareddata/swiftpm/Package.resolved | 27 +++---- .../xcshareddata/xcschemes/NAMS Muse.xcscheme | 4 + .../xcschemes/NAMS Vision.xcscheme | 77 ------------------- 4 files changed, 26 insertions(+), 104 deletions(-) delete mode 100644 NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Vision.xcscheme diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index c489e77..e177ef2 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -1559,6 +1559,7 @@ DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + "DEVELOPMENT_TEAM[sdk=xros*]" = 637867499T; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1592,6 +1593,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi 2"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1793,17 +1795,17 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MUSE"; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1855,16 +1857,16 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = MUSE; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1945,6 +1947,7 @@ DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 637867499T; + "DEVELOPMENT_TEAM[sdk=xros*]" = 637867499T; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1978,6 +1981,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi 2"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2039,17 +2043,17 @@ PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.bdhg.nams; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "TEST MUSE"; "SWIFT_ELicenseRef-NAMS_LOC_STRINGS" = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NAMS/Supporting Files/NAMS-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Test; }; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8329bd2..edf0f0f 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,12 @@ { "pins" : [ { - "identity" : "abseil-cpp-swiftpm", + "identity" : "abseil-cpp-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git", + "location" : "https://github.com/google/abseil-cpp-binary.git", "state" : { - "revision" : "06e7506b74bfc47c70f3353e2927ea3e2275230c", - "version" : "0.20220623.1" + "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", + "version" : "1.2022062300.0" } }, { @@ -18,15 +18,6 @@ "version" : "10.18.1" } }, - { - "identity" : "boringssl-swiftpm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/boringssl-SwiftPM.git", - "state" : { - "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab", - "version" : "0.9.1" - } - }, { "identity" : "fhirmodels", "kind" : "remoteSourceControl", @@ -73,12 +64,12 @@ } }, { - "identity" : "grpc-ios", + "identity" : "grpc-binary", "kind" : "remoteSourceControl", - "location" : "https://github.com/grpc/grpc-ios.git", + "location" : "https://github.com/google/grpc-binary.git", "state" : { - "revision" : "da8a2405e9fdf3dfb127cefc5af56c29be8df2f2", - "version" : "1.49.2" + "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", + "version" : "1.49.1" } }, { @@ -231,7 +222,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { "branch" : "feature/platform-support", - "revision" : "a469d40b0a02dffd77807c50db89c54f6af629e7" + "revision" : "a3fa40cc2f226200ece7c520048451edcc01812b" } }, { diff --git a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme index de9221c..bfbc097 100644 --- a/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme +++ b/NAMS.xcodeproj/xcshareddata/xcschemes/NAMS Muse.xcscheme @@ -49,6 +49,10 @@ ReferencedContainer = "container:NAMS.xcodeproj"> + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - From e046a41aea87de8b9a63664139f351da9b473ef6 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 12:15:41 -0800 Subject: [PATCH 11/21] Minor changes and BLE stability improvements --- NAMS.xcodeproj/project.pbxproj | 14 +++--- .../xcshareddata/swiftpm/Package.resolved | 26 +++++------ NAMS/Devices/Biopot/BiopotDevice.swift | 1 - .../Biopot/Views/BiopotDeviceRow.swift | 1 - NAMS/Devices/DeviceCoordinator.swift | 36 ++++++++++++++- NAMS/Devices/Muse/MuseDeviceManager.swift | 1 + NAMS/Devices/NearbyDevicesView.swift | 26 +++++------ NAMS/EEG/EEGRecordings.swift | 6 ++- NAMS/EEG/{Chart => }/StartRecordingView.swift | 0 NAMS/Home.swift | 9 +--- NAMS/ObsevableTests.swift | 45 +++++++++++++++++++ NAMS/Patients/Tasks/CompletedTask.swift | 10 +++-- NAMS/ScheduleView.swift | 8 ++-- 13 files changed, 132 insertions(+), 51 deletions(-) rename NAMS/EEG/{Chart => }/StartRecordingView.swift (100%) create mode 100644 NAMS/ObsevableTests.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index e177ef2..07c2da9 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -260,6 +260,7 @@ A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D83F922B081A47000D0C78 /* BiopotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D83F912B081A47000D0C78 /* BiopotTests.swift */; }; + A9DA40002B72D65600012E6A /* ObsevableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */; }; A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; A9DF79DF2AE8A81F00AB5983 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; @@ -422,6 +423,7 @@ A9D4B8E82B6863D60054E27C /* FitLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitLabel.swift; sourceTree = ""; }; A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTileHeader.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; + A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObsevableTests.swift; sourceTree = ""; }; A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPatientCard.swift; sourceTree = ""; }; A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTile.swift; sourceTree = ""; }; A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTile.swift; sourceTree = ""; }; @@ -596,6 +598,7 @@ 2FC9759D2978E30800BA99FE /* Supporting Files */, A945340E2AEAF2860095AAD3 /* Tiles */, A9BCB58E2AE8586A00DA8588 /* Utils */, + A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */, ); path = NAMS; sourceTree = ""; @@ -636,6 +639,7 @@ A926D8102AB7B430000C4C2F /* Recording */, A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */, 2DC171E38A5303E2CE997FF6 /* EEGRecordings.swift */, + A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */, ); path = EEG; sourceTree = ""; @@ -746,7 +750,6 @@ A926D80E2AB7B430000C4C2F /* EEGChannelMark.swift */, A926D8142AB7B430000C4C2F /* EEGChart.swift */, A926D80F2AB7B430000C4C2F /* EEGRecording.swift */, - A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */, ); path = Chart; sourceTree = ""; @@ -1240,6 +1243,7 @@ A9F2ECCD2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */, A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */, + A9DA40002B72D65600012E6A /* ObsevableTests.swift in Sources */, A907DA3F2B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, A9D4B8E62B6860AC0054E27C /* MuseHeadbandFitView.swift in Sources */, @@ -1593,7 +1597,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1981,7 +1985,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Stanford BDHG NeuroNest - Development Andi 2"; - "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi 2"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=xros*]" = "Stanford BDHG NeuroNest - Development Andi"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2212,8 +2216,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziViews.git"; requirement = { - branch = "feature/platform-support"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 1.2.0; }; }; 2FE5DC9029EDD9C3004B9AB4 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index edf0f0f..3f54088 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk.git", "state" : { - "revision" : "b880ec8ec927a838c51c12862c6222c30d7097d7", - "version" : "10.20.0" + "revision" : "f91c8167141d0279726c6f6d9d4a47c026785cbc", + "version" : "10.21.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "ceec9f28dea12b7cf3dabf18b5ed7621c88fd4aa", - "version" : "10.20.0" + "revision" : "cb8617fab75d181270a1d8f763f26b15c73e2e1e", + "version" : "10.21.0" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/ResearchKit", "state" : { - "revision" : "209164ed20592a2213c4bd69cefcb078d9de0692", - "version" : "2.2.21" + "revision" : "66f2fca769dc103de5801bb491b1d8ceafcd281e", + "version" : "2.2.23" } }, { @@ -159,7 +159,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { "branch" : "feature/unit-testing-setup", - "revision" : "ba022198596e3d8e6573ef0868ea8089aa0d22dc" + "revision" : "974e52e1f197caa1191a25741e0d88edafabe353" } }, { @@ -185,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", "state" : { - "revision" : "d1e6d4cddcf236038d21a73d671806d8ba51b01c", - "version" : "1.0.1" + "revision" : "0346857e2f1d6fd4b1d950d271be6c82df97107f", + "version" : "1.0.2" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "branch" : "feature/platform-support", - "revision" : "a3fa40cc2f226200ece7c520048451edcc01812b" + "revision" : "7210f72d6821d2eeb93438b29cb854a8ce334164", + "version" : "1.2.0" } }, { @@ -239,8 +239,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index 848dc50..384167d 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -85,7 +85,6 @@ class BiopotDevice: BluetoothDevice, Identifiable { @MainActor private func handleChange(of state: PeripheralState) { - // TODO: this is not called if the device is instantly destroyed! logger.debug("Biopot device is now \(state)") if state == .disconnected || state == .disconnecting { diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index fc3b5d3..cef208f 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -20,7 +20,6 @@ struct BiopotDeviceRow: View { var body: some View { - // TODO: how to UI test the biopot? NearbyDeviceRow(peripheral: device) { Task { await deviceCoordinator.tapDevice(.biopot(device)) diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index d6bb033..7376caf 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -6,10 +6,10 @@ // SPDX-License-Identifier: MIT // -import Foundation import OSLog import Spezi import SpeziBluetooth +import SwiftUI @Observable @@ -18,10 +18,44 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { private(set) var connectedDevice: ConnectedDevice? + @AppStorage(StorageKeys.autoConnect) + @ObservationIgnored private var _autoConnect = false + @AppStorage(StorageKeys.autoConnectBackground) + @ObservationIgnored private var _autoConnectBackground = false + var isConnected: Bool { connectedDevice != nil } + var autoConnect: Bool { + get { + access(keyPath: \.autoConnect) + return _autoConnect + } + set { + withMutation(keyPath: \.autoConnect) { + _autoConnect = newValue + } + } + } + + var autoConnectBackground: Bool { + get { + access(keyPath: \.autoConnectBackground) + return _autoConnectBackground + } + set { + withMutation(keyPath: \.autoConnectBackground) { + _autoConnectBackground = newValue + } + } + } + + + var shouldAutoConnectBiopot: Bool { + _autoConnectBackground && !isConnected + } + required init() {} diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 2f6c70d..1fccfae 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -23,6 +23,7 @@ class MuseDeviceManager { private(set) var nearbyMuses: [MuseDevice] = [] init() { + // TODO: sometimes devices are stale after app open. Look into checking advertising stats or just reconstructing the whole muse manager if nothing is connected upon scanning? self.museManager = IXNMuseManagerIos() self.museListener = MuseListener(deviceManager: self) diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index 9ba82e4..92e96ae 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -25,16 +25,11 @@ struct NearbyDevicesView: View { private var museDeviceManager #endif @Environment(MockDeviceManager.self) - private var mockDeviceManager: MockDeviceManager? // TODO: flag to disable mock devices! + private var mockDeviceManager: MockDeviceManager? @Environment(\.dismiss) private var dismiss - @AppStorage(StorageKeys.autoConnect) - private var autoConnect = false - @AppStorage(StorageKeys.autoConnectBackground) - private var autoConnectBackground = false // TODO: declared twice? - private var consideredPoweredOn: Bool { mockDeviceManager != nil || bluetooth.state == .poweredOn @@ -46,21 +41,23 @@ struct NearbyDevicesView: View { } var body: some View { + @Bindable var deviceCoordinator = deviceCoordinator // TODO: remove closure length! // swiftlint:disable:next closure_body_length NavigationStack { List { Section { - Toggle("Auto Connect", isOn: $autoConnect) - if autoConnect { - Toggle("Continuous Background Search", isOn: $autoConnectBackground) // TODO: make it a selection navigation destination? + // TODO: AutoConnect feature: On, In Background, Off + Toggle("Auto Connect", isOn: $deviceCoordinator.autoConnect) + if deviceCoordinator.autoConnect { + // TODO: make it a selection navigation destination? + Toggle("Continuous Background Search", isOn: $deviceCoordinator.autoConnectBackground) } } footer: { Text("Automatically connect to nearby SensoMedical BIOPOT3 devices.") } if consideredPoweredOn { - // TODO: sort all devices by initial discovery (descending?, latest at the top!) Section { #if MUSE MuseDeviceList() @@ -79,7 +76,7 @@ struct NearbyDevicesView: View { } header: { LoadingSectionHeaderView("Devices", loading: isScanning) } footer: { - MuseTroublesConnectingHint() // TODO: that doesn't apply to all devices? + MuseTroublesConnectingHint() } } else { Section { @@ -95,8 +92,7 @@ struct NearbyDevicesView: View { } } } - // TODO: auto-connect also conflicts with Muse devices? (enough to disable autoConnect if we have something connected?) - .scanNearbyDevices(with: bluetooth, autoConnect: autoConnect && !deviceCoordinator.isConnected) + .scanNearbyDevices(with: bluetooth, autoConnect: deviceCoordinator.shouldAutoConnectBiopot) .scanNearbyDevices(enabled: mockDeviceManager != nil, with: mockDeviceManager ?? MockDeviceManager()) #if MUSE .scanNearbyDevices(enabled: bluetooth.state == .poweredOn, with: museDeviceManager) @@ -126,7 +122,7 @@ struct NearbyDevicesView: View { } } #if MUSE - .environment(MuseDeviceManager()) // TODO: make this a module? + .environment(MuseDeviceManager()) #endif .environment(MockDeviceManager()) } @@ -140,7 +136,7 @@ struct NearbyDevicesView: View { } } #if MUSE - .environment(MuseDeviceManager()) // TODO: make this a module? + .environment(MuseDeviceManager()) #endif .environment(MockDeviceManager()) } diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift index b6b1c56..9e2a0db 100644 --- a/NAMS/EEG/EEGRecordings.swift +++ b/NAMS/EEG/EEGRecordings.swift @@ -42,7 +42,6 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { @MainActor func startRecordingSession() async throws { let session = EEGRecordingSession() - self.recordingSession = session guard let device = deviceCoordinator.connectedDevice else { throw EEGRecordingError.noConnectedDevice @@ -50,6 +49,11 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { // TODO: handle the case where the device disconnects when an ongoing recording is in progress? => Issue try await device.startRecording(session) + + // We set the recording session after recording was enabled on the device. + // Otherwise, we would navigate away to early from the Splash screen and would result in this + // task being cancelled. + self.recordingSession = session } @MainActor diff --git a/NAMS/EEG/Chart/StartRecordingView.swift b/NAMS/EEG/StartRecordingView.swift similarity index 100% rename from NAMS/EEG/Chart/StartRecordingView.swift rename to NAMS/EEG/StartRecordingView.swift diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 879d418..1604d9f 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -46,9 +46,6 @@ struct HomeView: View { @State private var viewState: ViewState = .idle @State private var presentingAccount = false - @AppStorage(StorageKeys.autoConnectBackground) - private var autoConnectBackground = false // TODO: does this binding update? - var body: some View { TabView(selection: $selectedTab) { ScheduleView(presentingAccount: $presentingAccount, activePatientId: $activePatientId) @@ -68,7 +65,7 @@ struct HomeView: View { #if MUSE .environment(museDeviceManager) #endif - .autoConnect(enabled: autoConnectBackground && !deviceCoordinator.isConnected, with: bluetooth) + .autoConnect(enabled: deviceCoordinator.shouldAutoConnectBiopot, with: bluetooth) .onAppear { if FeatureFlags.injectDefaultPatient { Task { @@ -91,11 +88,9 @@ struct HomeView: View { guard let biopot else { return } - // TODO: we kinda also need connecting state! + // TODO: we kinda also need connecting state!? just change Bluetooth behavior? // a new device is connected now - // TODO: remove - print("NEW BIOPOT WITH STATE \(biopot.state)") deviceCoordinator.notifyConnectedDevice(.biopot(biopot)) } .onChange(of: viewState) { oldValue, newValue in diff --git a/NAMS/ObsevableTests.swift b/NAMS/ObsevableTests.swift new file mode 100644 index 0000000..67d413a --- /dev/null +++ b/NAMS/ObsevableTests.swift @@ -0,0 +1,45 @@ +// +// ObsevableTests.swift +// NAMS +// +// Created by Andreas Bauer on 06.02.24. +// + +import SwiftUI + +@Observable +class SomeModel { + var counter: Int = 0 + + func task() { + Task { + for _ in 1...1000 { + withMutation(keyPath: \.counter) { + print("changed to \(counter + 1)") + _counter += 1 + // counter += 1 + } + } + } + } +} + +struct ObsevableTests: View { + @State var myModel = SomeModel() + var body: some View { + let ssa = print("Refreshed with \(myModel.counter)") + Text("Counter: \(myModel.counter)") + .onChange(of: myModel.counter) { + print("Changed with \(myModel.counter)") + } + + Button("Start") { + myModel.counter = 0 + myModel.task() + } + } +} + +#Preview { + ObsevableTests() +} diff --git a/NAMS/Patients/Tasks/CompletedTask.swift b/NAMS/Patients/Tasks/CompletedTask.swift index b85e697..d7444a6 100644 --- a/NAMS/Patients/Tasks/CompletedTask.swift +++ b/NAMS/Patients/Tasks/CompletedTask.swift @@ -15,6 +15,8 @@ import SpeziQuestionnaire enum TaskContent { #if canImport(SpeziQuestionnaire) case questionnaireResponse(_ response: QuestionnaireResponse) + #else + case questionnaireResponse #endif case eegRecording // currently empty } @@ -27,7 +29,7 @@ struct CompletedTask: Codable { } -extension TaskContent: Codable { // TODO: this codable conformance breaks with visionOs? is that a big deal? +extension TaskContent: Codable { private enum CodingKeys: String, CodingKey { case type case questionnaireResponse @@ -46,11 +48,13 @@ extension TaskContent: Codable { // TODO: this codable conformance breaks with v let type = try container.decode(String.self, forKey: .type) switch type { - #if canImport(SpeziQuestionnaire) case Self.questionnaireResponseType: + #if canImport(SpeziQuestionnaire) let response = try container.decode(QuestionnaireResponse.self, forKey: .questionnaireResponse) self = .questionnaireResponse(response) - #endif + #else + self = .questionnaireResponse + #endif case Self.eegRecordingType: self = .eegRecording default: diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index 1a575e5..bf2e8a1 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -16,11 +16,11 @@ struct ScheduleView: View { @Environment(BiopotDevice.self) private var biopot: BiopotDevice? - @State var presentingMuseList = false - @State var presentPatientSheet = false - @Binding var presentingAccount: Bool + @State private var presentingMuseList = false + @State private var presentPatientSheet = false + @Binding private var presentingAccount: Bool - @Binding var activePatientId: String? + @Binding private var activePatientId: String? var body: some View { NavigationStack { From 1c65c6aedcc27df551574270f5968fc298f30da4 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 17:00:13 -0800 Subject: [PATCH 12/21] Update to the latest SpeziBluetooth and get tests working --- NAMS.xcodeproj/project.pbxproj | 24 ++++- .../xcshareddata/swiftpm/Package.resolved | 2 +- NAMS/Devices/AutoConnectConfiguration.swift | 27 +++++ .../AutoConnectConfigurationView.swift | 45 +++++++++ NAMS/Devices/Biopot/BiopotDevice.swift | 43 +++++++- .../Views/BiopotDeviceDetailsView.swift | 98 ++++++++++++++----- .../Biopot/Views/BiopotDeviceRow.swift | 14 ++- NAMS/Devices/DeviceCoordinator.swift | 34 ++----- NAMS/Devices/Mock/Views/MockDeviceRow.swift | 19 +++- .../Views/Details/MuseDeviceDetailsView.swift | 3 +- .../Details/MuseHeadbandFitSection.swift | 2 +- NAMS/Devices/Muse/Views/MuseDeviceRow.swift | 18 +++- NAMS/Devices/NearbyDevicesView.swift | 85 ++++++++++------ NAMS/EEG/Chart/EEGRecording.swift | 8 +- NAMS/Home.swift | 27 ++--- NAMS/ObsevableTests.swift | 45 --------- NAMS/Patients/CurrentPatientLabel.swift | 10 +- NAMS/Patients/Model/PatientListModel.swift | 43 ++++++-- NAMS/Patients/PatientInformation.swift | 14 +-- NAMS/Patients/PatientList.swift | 24 +++-- NAMS/Patients/PatientListSheet.swift | 16 +-- NAMS/Patients/PatientRow.swift | 32 +++--- NAMS/Patients/SelectedPatientCard.swift | 11 +-- NAMS/Resources/Localizable.xcstrings | 24 ++++- NAMS/ScheduleView.swift | 19 ++-- NAMS/Tiles/TilesView.swift | 2 +- NAMS/Utils/StorageKeys.swift | 3 +- NAMSTests/BiopotCodingTests.swift | 73 ++------------ NAMSTests/BiopotDeviceTests.swift | 15 +-- NAMSUITests/BiopotTests.swift | 36 ++++--- NAMSUITests/MockDeviceTests.swift | 33 +++++-- NAMSUITests/QuestionnaireTests.swift | 26 ++--- 32 files changed, 517 insertions(+), 358 deletions(-) create mode 100644 NAMS/Devices/AutoConnectConfiguration.swift create mode 100644 NAMS/Devices/AutoConnectConfigurationView.swift delete mode 100644 NAMS/ObsevableTests.swift diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 07c2da9..d604a51 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -169,6 +169,11 @@ A92BB0392B6C204600788753 /* M_CHAT_R_F-en-US-v1.1.json in Resources */ = {isa = PBXBuildFile; fileRef = A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */; }; A92E34F02ADB9B7E00FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34EF2ADB9B7E00FE0B51 /* OrderedCollections */; }; A92E34F22ADB9B9000FE0B51 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = A92E34F12ADB9B9000FE0B51 /* OrderedCollections */; }; + A93B82C02B76E2E000C5DF3D /* AutoConnectConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93B82BF2B76E2E000C5DF3D /* AutoConnectConfigurationView.swift */; }; + A93B82C12B76E2E000C5DF3D /* AutoConnectConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93B82BF2B76E2E000C5DF3D /* AutoConnectConfigurationView.swift */; }; + A93B82C32B76E72200C5DF3D /* AutoConnectConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93B82C22B76E72200C5DF3D /* AutoConnectConfiguration.swift */; }; + A93B82C42B76E72200C5DF3D /* AutoConnectConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93B82C22B76E72200C5DF3D /* AutoConnectConfiguration.swift */; }; + A93B82CF2B76EE0700C5DF3D /* XCTBluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = A93B82CE2B76EE0700C5DF3D /* XCTBluetooth */; }; A9405B562A36856300C75412 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; A94533FA2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */; }; A94533FB2AEADC8E0095AAD3 /* ScreeningTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */; }; @@ -260,7 +265,6 @@ A9D4B8EC2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D4B8ED2B686D380054E27C /* ScreeningTileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */; }; A9D83F922B081A47000D0C78 /* BiopotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D83F912B081A47000D0C78 /* BiopotTests.swift */; }; - A9DA40002B72D65600012E6A /* ObsevableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */; }; A9DF79DA2AE8986B00AB5983 /* SelectedPatientCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */; }; A9DF79DE2AE8A80D00AB5983 /* Patient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98911312A36689D00E66E3A /* Patient.swift */; }; A9DF79DF2AE8A81F00AB5983 /* AddPatientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9405B552A36856300C75412 /* AddPatientView.swift */; }; @@ -376,6 +380,8 @@ A926D8132AB7B430000C4C2F /* EEGReading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGReading.swift; sourceTree = ""; }; A926D8142AB7B430000C4C2F /* EEGChart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EEGChart.swift; sourceTree = ""; }; A92BB0372B6C1F7600788753 /* M_CHAT_R_F-en-US-v1.1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "M_CHAT_R_F-en-US-v1.1.json"; sourceTree = ""; }; + A93B82BF2B76E2E000C5DF3D /* AutoConnectConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoConnectConfigurationView.swift; sourceTree = ""; }; + A93B82C22B76E72200C5DF3D /* AutoConnectConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoConnectConfiguration.swift; sourceTree = ""; }; A9405B552A36856300C75412 /* AddPatientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPatientView.swift; sourceTree = ""; }; A94533F92AEADC8E0095AAD3 /* ScreeningTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTile.swift; sourceTree = ""; }; A94533FC2AEADCC00095AAD3 /* ScreeningTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTask.swift; sourceTree = ""; }; @@ -423,7 +429,6 @@ A9D4B8E82B6863D60054E27C /* FitLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FitLabel.swift; sourceTree = ""; }; A9D4B8EB2B686D380054E27C /* ScreeningTileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreeningTileHeader.swift; sourceTree = ""; }; A9D83F912B081A47000D0C78 /* BiopotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotTests.swift; sourceTree = ""; }; - A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObsevableTests.swift; sourceTree = ""; }; A9DF79D92AE8986B00AB5983 /* SelectedPatientCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedPatientCard.swift; sourceTree = ""; }; A9F2ECC52AEB27B10057C7DD /* MeasurementTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementTile.swift; sourceTree = ""; }; A9F2ECC82AEC2C300057C7DD /* CompletedTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedTile.swift; sourceTree = ""; }; @@ -461,6 +466,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A93B82CF2B76EE0700C5DF3D /* XCTBluetooth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -598,7 +604,6 @@ 2FC9759D2978E30800BA99FE /* Supporting Files */, A945340E2AEAF2860095AAD3 /* Tiles */, A9BCB58E2AE8586A00DA8588 /* Utils */, - A9DA3FFF2B72D65600012E6A /* ObsevableTests.swift */, ); path = NAMS; sourceTree = ""; @@ -850,10 +855,12 @@ A988FEAE2B04529B00022A61 /* Biopot */, A926D8342AB7C2CC000C4C2F /* Mock */, A99522402AA61D82009272F4 /* Muse */, + A93B82BF2B76E2E000C5DF3D /* AutoConnectConfigurationView.swift */, A988FEAB2B043AED00022A61 /* BatteryIcon.swift */, A9CE84502B1A9A14009CE3F4 /* NearbyDevicesView.swift */, 2DC172078D2802AD6068AC7E /* DeviceCoordinator.swift */, 2DC17AEF81E62461D4AA3E67 /* ConnectedDevice.swift */, + A93B82C22B76E72200C5DF3D /* AutoConnectConfiguration.swift */, ); path = Devices; sourceTree = ""; @@ -956,6 +963,7 @@ ); name = NAMSTests; packageProductDependencies = ( + A93B82CE2B76EE0700C5DF3D /* XCTBluetooth */, ); productName = NAMSTests; productReference = 653A255D28338800005D4D48 /* NAMSTests.xctest */; @@ -1224,6 +1232,7 @@ A9BCB5892AE83F7E00DA8588 /* PatientListModel.swift in Sources */, A926D8262AB7B430000C4C2F /* EEGChannel.swift in Sources */, A926D8032AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, + A93B82C32B76E72200C5DF3D /* AutoConnectConfiguration.swift in Sources */, A926D8282AB7B430000C4C2F /* EEGReading.swift in Sources */, A9BCB57F2AE82FFC00DA8588 /* PatientSearchModel.swift in Sources */, 2DC171B41D4A87E2C861C356 /* ConnectionState+Muse.swift in Sources */, @@ -1236,6 +1245,7 @@ 2DC17FE0AC1DD98C29B417F1 /* IXNMusePreset+Description.swift in Sources */, 2DC175AEF3E3A716DF747E21 /* EEGFrequency.swift in Sources */, 2DC179D0C1180EF9B1E8F276 /* MuseDeviceManager.swift in Sources */, + A93B82C02B76E2E000C5DF3D /* AutoConnectConfigurationView.swift in Sources */, A94534132AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC173E02BF55765A906AF4F /* IXNMuseDataPacketType+Type.swift in Sources */, A945340C2AEAE6380095AAD3 /* TilesView.swift in Sources */, @@ -1243,7 +1253,6 @@ A9F2ECCD2AEC58B00057C7DD /* SimpleTile.swift in Sources */, 2DC179421EE83DA24520EABB /* HeadbandFit+Muse.swift in Sources */, A97E4F1F2B1EA0D600E25505 /* StartRecordingView.swift in Sources */, - A9DA40002B72D65600012E6A /* ObsevableTests.swift in Sources */, A907DA3F2B1964B500FB69FB /* ByteBuffer+Int24.swift in Sources */, A9CE84512B1A9A14009CE3F4 /* NearbyDevicesView.swift in Sources */, A9D4B8E62B6860AC0054E27C /* MuseHeadbandFitView.swift in Sources */, @@ -1362,6 +1371,7 @@ A926D79D2AB7A552000C4C2F /* Contacts.swift in Sources */, A926D79E2AB7A552000C4C2F /* FinishedSetup.swift in Sources */, A926D8272AB7B430000C4C2F /* EEGChannel.swift in Sources */, + A93B82C42B76E72200C5DF3D /* AutoConnectConfiguration.swift in Sources */, A926D8042AB7B41C000C4C2F /* EEGReading+Muse.swift in Sources */, A926D8292AB7B430000C4C2F /* EEGReading.swift in Sources */, A988FEAD2B043AED00022A61 /* BatteryIcon.swift in Sources */, @@ -1374,6 +1384,7 @@ A988FEB32B0452C400022A61 /* DeviceConfiguration.swift in Sources */, 2DC173E694F96E89EAB63FE0 /* IXNMuseConfiguration+Description.swift in Sources */, 2DC17924E7D44FE1A2562528 /* IXNMusePreset+Description.swift in Sources */, + A93B82C12B76E2E000C5DF3D /* AutoConnectConfigurationView.swift in Sources */, 2DC17D159C62F690B2137E65 /* EEGFrequency.swift in Sources */, A94534142AEAFB0E0095AAD3 /* PatientListModel+QuestionnaireResponse.swift in Sources */, 2DC1713C311BAA65EE0E2748 /* MuseDeviceManager.swift in Sources */, @@ -2444,6 +2455,11 @@ package = A92E34EE2ADB9B7E00FE0B51 /* XCRemoteSwiftPackageReference "swift-collections" */; productName = OrderedCollections; }; + A93B82CE2B76EE0700C5DF3D /* XCTBluetooth */ = { + isa = XCSwiftPackageProductDependency; + package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; + productName = XCTBluetooth; + }; A95EEAE22B6B54D3009B4CF8 /* BluetoothViews */ = { isa = XCSwiftPackageProductDependency; package = A988FEA42B03FB4A00022A61 /* XCRemoteSwiftPackageReference "SpeziBluetooth" */; diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3f54088..4578558 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -159,7 +159,7 @@ "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { "branch" : "feature/unit-testing-setup", - "revision" : "974e52e1f197caa1191a25741e0d88edafabe353" + "revision" : "16ce491c3f24c297ff55351c04bd87402af1a7bf" } }, { diff --git a/NAMS/Devices/AutoConnectConfiguration.swift b/NAMS/Devices/AutoConnectConfiguration.swift new file mode 100644 index 0000000..a4a9ad8 --- /dev/null +++ b/NAMS/Devices/AutoConnectConfiguration.swift @@ -0,0 +1,27 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +enum AutoConnectConfiguration: String, CustomLocalizedStringResourceConvertible { + case off + case on // swiftlint:disable:this identifier_name + case background + + var localizedStringResource: LocalizedStringResource { + switch self { + case .off: + return "Off" + case .on: + return "On" + case .background: + return "In Background" + } + } +} diff --git a/NAMS/Devices/AutoConnectConfigurationView.swift b/NAMS/Devices/AutoConnectConfigurationView.swift new file mode 100644 index 0000000..9408d53 --- /dev/null +++ b/NAMS/Devices/AutoConnectConfigurationView.swift @@ -0,0 +1,45 @@ +// +// This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +// +// SPDX-FileCopyrightText: 2024 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct AutoConnectConfigurationView: View { + @Environment(DeviceCoordinator.self) + private var deviceCoordinator + + var body: some View { + @Bindable var deviceCoordinator = deviceCoordinator + List { + Section { + Picker(selection: $deviceCoordinator.autoConnectOption) { + Text("Off").tag(AutoConnectConfiguration.off) + Text("On").tag(AutoConnectConfiguration.on) + Text("Search in Background").tag(AutoConnectConfiguration.background) + } label: { + EmptyView() + } + .pickerStyle(.inline) + } footer: { + Text("Automatically connect to nearby SensoMedical BIOPOT3 devices.") + + Text(verbatim: "\n") + + Text("If background search is enabled, the application will search for nearby devices until a devices was found and connected.") + } + } + .navigationTitle("Auto Connect") + .navigationBarTitleDisplayMode(.inline) + } +} + + +#Preview { + NavigationStack { + AutoConnectConfigurationView() + } + .environment(DeviceCoordinator()) +} diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index 384167d..a403a51 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -10,7 +10,8 @@ import BluetoothServices import BluetoothViews import OSLog import Spezi -@_spi(TestingSupport) import SpeziBluetooth // swiftlint:disable:this attributes +@_spi(TestingSupport) +import SpeziBluetooth import class CoreBluetooth.CBUUID @@ -212,3 +213,43 @@ extension BiopotDevice: GenericBluetoothPeripheral { } } } + + +extension BiopotDevice { + static func createMock(serial: String = "0xAABBCCDD", state: PeripheralState = .disconnected) -> BiopotDevice { + let biopot = BiopotDevice() + biopot.service.$deviceInfo.inject(DeviceInformation( + syncRatio: 0, + syncMode: false, + memoryWriteNumber: 0, + memoryEraseMode: false, + batteryLevel: 75, + temperatureValue: 23, + batteryCharging: true + )) + biopot.deviceInformation.$firmwareRevision.inject("1.2.3") + biopot.deviceInformation.$serialNumber.inject(serial) + biopot.deviceInformation.$hardwareRevision.inject("3.1") + + biopot.$id.inject(UUID()) + biopot.$name.inject("SML BIO \(serial)") + biopot.$state.inject(state) + + biopot.$connect.inject { @MainActor [weak biopot] in + biopot?.$state.inject(.connecting) + biopot?.handleChange(of: .connecting) + + try? await Task.sleep(for: .seconds(1)) + + biopot?.$state.inject(.connected) + biopot?.handleChange(of: .connected) + } + + biopot.$disconnect.inject { @MainActor [weak biopot] in + biopot?.$state.inject(.disconnected) + biopot?.handleChange(of: .disconnected) + } + + return biopot + } +} diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift index 0477a46..902fbc7 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceDetailsView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziBluetooth import SpeziViews import SwiftUI @@ -17,6 +18,25 @@ struct BiopotDeviceDetailsView: View { @Environment(\.dismiss) private var dismiss + var hasAboutInformation: Bool { + let deviceInformation = biopot.deviceInformation + return deviceInformation.firmwareRevision != nil + || deviceInformation.hardwareRevision != nil + || deviceInformation.serialNumber != nil + } + + var title: Text { + if let name = biopot.name { + Text(verbatim: name) + } else { + Text("Unknown Device") + } + } + + var isDisconnected: Bool { + biopot.state == .disconnected || biopot.state == .disconnecting + } + var body: some View { List { if let info = biopot.service.deviceInfo { @@ -27,40 +47,51 @@ struct BiopotDeviceDetailsView: View { } } - Section("About") { - if let firmware = biopot.deviceInformation.firmwareRevision { - ListRow("Firmware Version") { - Text(verbatim: firmware) + if hasAboutInformation { + Section("About") { + if let firmware = biopot.deviceInformation.firmwareRevision { + ListRow("Firmware Version") { + Text(verbatim: firmware) + } } - } - if let hardware = biopot.deviceInformation.hardwareRevision { - ListRow("Hardware Version") { - Text(verbatim: hardware) + if let hardware = biopot.deviceInformation.hardwareRevision { + ListRow("Hardware Version") { + Text(verbatim: hardware) + } } - } - if let serialNumber = biopot.deviceInformation.serialNumber { - ListRow("Serial Number") { - Text(verbatim: serialNumber) + if let serialNumber = biopot.deviceInformation.serialNumber { + ListRow("Serial Number") { + Text(verbatim: serialNumber) + } } } } + disconnectButton + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(title) + .onChange(of: biopot.state) { + if isDisconnected { + dismiss() + } + } + } + + @ViewBuilder var disconnectButton: some View { + Section { Button(action: { disconnectClosure() - dismiss() }) { Text("Disconnect") .frame(maxWidth: .infinity) } - .disabled(biopot.state == .disconnected || biopot.state == .disconnecting) - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text(verbatim: biopot.name ?? "")) - .onChange(of: biopot.state) { - if biopot.state == .disconnected || biopot.state == .disconnecting { - dismiss() - } + .disabled(isDisconnected) + } footer: { + if isDisconnected { + Text("This device is no longer connected.") } + } } @@ -71,4 +102,27 @@ struct BiopotDeviceDetailsView: View { } -// TODO: preview +#if DEBUG +#Preview { + let biopot = BiopotDevice.createMock(state: .connected) + + return NavigationStack { + BiopotDeviceDetailsView(device: biopot) { + Task { + await biopot.disconnect() + } + } + } +} + +#Preview { + let biopot = BiopotDevice.createMock() + return NavigationStack { + BiopotDeviceDetailsView(device: biopot) { + Task { + await biopot.disconnect() + } + } + } +} +#endif diff --git a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift index cef208f..9fe3bc6 100644 --- a/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift +++ b/NAMS/Devices/Biopot/Views/BiopotDeviceRow.swift @@ -7,6 +7,7 @@ // import BluetoothViews +import SpeziBluetooth import SwiftUI @@ -42,4 +43,15 @@ struct BiopotDeviceRow: View { } -// TODO: preview +#if DEBUG +#Preview { + let biopot = BiopotDevice.createMock(state: .connected) + return NavigationStack { + List { + BiopotDeviceRow(device: biopot) + BiopotDeviceRow(device: BiopotDevice.createMock(serial: "0xDDEEFFGG")) + } + } + .environment(DeviceCoordinator(mock: .biopot(biopot))) +} +#endif diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index 7376caf..6eb5bb5 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -18,42 +18,28 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { private(set) var connectedDevice: ConnectedDevice? - @AppStorage(StorageKeys.autoConnect) - @ObservationIgnored private var _autoConnect = false - @AppStorage(StorageKeys.autoConnectBackground) - @ObservationIgnored private var _autoConnectBackground = false + @AppStorage(StorageKeys.autoConnectOption) + @ObservationIgnored private var _autoConnectOption: AutoConnectConfiguration = .off var isConnected: Bool { connectedDevice != nil } - var autoConnect: Bool { + var autoConnectOption: AutoConnectConfiguration { get { - access(keyPath: \.autoConnect) - return _autoConnect + access(keyPath: \.autoConnectOption) + return _autoConnectOption } set { - withMutation(keyPath: \.autoConnect) { - _autoConnect = newValue - } - } - } - - var autoConnectBackground: Bool { - get { - access(keyPath: \.autoConnectBackground) - return _autoConnectBackground - } - set { - withMutation(keyPath: \.autoConnectBackground) { - _autoConnectBackground = newValue + withMutation(keyPath: \.autoConnectOption) { + _autoConnectOption = newValue } } } var shouldAutoConnectBiopot: Bool { - _autoConnectBackground && !isConnected + autoConnectOption == .background && !isConnected } @@ -61,8 +47,8 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { /// Shorthand for easy previewing devices. - init(mock: MockDevice) { - self.connectedDevice = .mock(mock) + init(mock: ConnectedDevice) { + self.connectedDevice = mock } diff --git a/NAMS/Devices/Mock/Views/MockDeviceRow.swift b/NAMS/Devices/Mock/Views/MockDeviceRow.swift index f829881..8ffe7ce 100644 --- a/NAMS/Devices/Mock/Views/MockDeviceRow.swift +++ b/NAMS/Devices/Mock/Views/MockDeviceRow.swift @@ -10,6 +10,21 @@ import BluetoothViews import SwiftUI +private struct MockDeviceDestination: View { + private let device: MockDevice + + var body: some View { + MuseDeviceDetailsView(model: device.label, state: device.connectionState, device.deviceInformation) { + device.disconnect() + } + } + + init(_ device: MockDevice) { + self.device = device + } +} + + struct MockDeviceRow: View { private let device: MockDevice @@ -28,9 +43,7 @@ struct MockDeviceRow: View { presentingActiveDevice = device } .navigationDestination(item: $presentingActiveDevice) { device in - MuseDeviceDetailsView(model: device.label, state: device.connectionState, device.deviceInformation) { - device.disconnect() - } + MockDeviceDestination(device) } } diff --git a/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift index ca5cff7..68f219e 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift @@ -32,7 +32,6 @@ struct MuseDeviceDetailsView: View { Button(action: { disconnectClosure() - dismiss() }) { Text("Disconnect") .frame(maxWidth: .infinity) @@ -42,7 +41,7 @@ struct MuseDeviceDetailsView: View { .navigationTitle(Text(verbatim: model)) .navigationBarTitleDisplayMode(.inline) .onChange(of: state) { - if state == .disconnected { + if state == .disconnected { // TODO: this doesn't work? dismiss() } } diff --git a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift index 8521d24..e711462 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseHeadbandFitSection.swift @@ -83,7 +83,7 @@ struct MuseHeadbandFitSection: View { } #Preview { - return NavigationStack { + NavigationStack { List { MuseHeadbandFitSection( .init(serialNumber: "0xAABBCC", firmwareVersion: "1.2", hardwareVersion: "20.1") diff --git a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift index c501f76..1c314d2 100644 --- a/NAMS/Devices/Muse/Views/MuseDeviceRow.swift +++ b/NAMS/Devices/Muse/Views/MuseDeviceRow.swift @@ -11,6 +11,20 @@ import SwiftUI #if MUSE +private struct MuseDeviceDestination: View { + private let device: MuseDevice + + var body: some View { + MuseDeviceDetailsView(model: device.model, state: device.connectionState, device.deviceInformation) { + device.disconnect() + } + } + + init(_ device: MuseDevice) { + self.device = device + } +} + struct MuseDeviceRow: View { private let device: MuseDevice @@ -28,9 +42,7 @@ struct MuseDeviceRow: View { presentingActiveDevice = device } .navigationDestination(item: $presentingActiveDevice) { device in - MuseDeviceDetailsView(model: device.model, state: device.connectionState, device.deviceInformation) { - device.disconnect() - } + MuseDeviceDestination(device) } } diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index 92e96ae..7f1a820 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -9,6 +9,7 @@ import BluetoothViews import Spezi import SpeziBluetooth +import SpeziViews import SwiftUI @@ -30,9 +31,17 @@ struct NearbyDevicesView: View { @Environment(\.dismiss) private var dismiss +#if targetEnvironment(simulator) + @State private var mockBiopot = BiopotDevice.createMock() +#endif + private var consideredPoweredOn: Bool { +#if targetEnvironment(simulator) + true // mock biopot is there in every case +#else mockDeviceManager != nil || bluetooth.state == .poweredOn +#endif } @@ -42,42 +51,13 @@ struct NearbyDevicesView: View { var body: some View { @Bindable var deviceCoordinator = deviceCoordinator - // TODO: remove closure length! - // swiftlint:disable:next closure_body_length + NavigationStack { List { - Section { - // TODO: AutoConnect feature: On, In Background, Off - Toggle("Auto Connect", isOn: $deviceCoordinator.autoConnect) - if deviceCoordinator.autoConnect { - // TODO: make it a selection navigation destination? - Toggle("Continuous Background Search", isOn: $deviceCoordinator.autoConnectBackground) - } - } footer: { - Text("Automatically connect to nearby SensoMedical BIOPOT3 devices.") - } + autoConnectLink if consideredPoweredOn { - Section { - #if MUSE - MuseDeviceList() - #endif - - let biopots = bluetooth.nearbyDevices(for: BiopotDevice.self) - ForEach(biopots) { biopot in - BiopotDeviceRow(device: biopot) - } - - if let mockDeviceManager { - ForEach(mockDeviceManager.nearbyDevices) { device in - MockDeviceRow(device: device) - } - } - } header: { - LoadingSectionHeaderView("Devices", loading: isScanning) - } footer: { - MuseTroublesConnectingHint() - } + nearbyDevicesSection } else { Section { BluetoothStateHint(bluetooth.state) @@ -107,6 +87,47 @@ struct NearbyDevicesView: View { #endif } + @ViewBuilder private var autoConnectLink: some View { + Section { + NavigationLink { + AutoConnectConfigurationView() + } label: { + ListRow("Auto Connect") { + Text(deviceCoordinator.autoConnectOption.localizedStringResource) + } + } + } footer: { + Text("Automatically connect to nearby SensoMedical BIOPOT3 devices.") + } + } + + @ViewBuilder private var nearbyDevicesSection: some View { + Section { +#if MUSE + MuseDeviceList() +#endif + +#if targetEnvironment(simulator) + BiopotDeviceRow(device: mockBiopot) +#endif + + let biopots = bluetooth.nearbyDevices(for: BiopotDevice.self) + ForEach(biopots) { biopot in + BiopotDeviceRow(device: biopot) + } + + if let mockDeviceManager { + ForEach(mockDeviceManager.nearbyDevices) { device in + MockDeviceRow(device: device) + } + } + } header: { + LoadingSectionHeaderView("Devices", loading: isScanning) + } footer: { + MuseTroublesConnectingHint() + } + } + init() {} } diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index caa1512..cb0a931 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -125,7 +125,7 @@ struct EEGRecording: View { #if DEBUG -#Preview { // TODO: verify previews +#Preview { let model = EEGRecordings() Task { @MainActor in try await model.startRecordingSession() @@ -133,9 +133,9 @@ struct EEGRecording: View { return NavigationStack { EEGRecording() .environment(PatientListModel()) - .environment(model) .previewWith { - DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) + model + DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) Bluetooth { Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) } @@ -149,7 +149,7 @@ struct EEGRecording: View { .environment(PatientListModel()) .previewWith { EEGRecordings() - DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) + DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) } } } diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 1604d9f..2be4f27 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -22,8 +22,6 @@ struct HomeView: View { @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule - @AppStorage(StorageKeys.selectedPatient) - private var activePatientId: String? @Environment(Account.self) private var account @@ -34,9 +32,10 @@ struct HomeView: View { @Environment(BiopotDevice.self) private var biopot: BiopotDevice? - // TODO: how to toggle mock device manager? - - @State var mockDeviceManager = MockDeviceManager() // TODO: = MockDeviceManager() +#if TEST || DEBUG + @State var mockDeviceManager = MockDeviceManager() +#endif + #if MUSE @State var museDeviceManager = MuseDeviceManager() #endif @@ -48,7 +47,7 @@ struct HomeView: View { var body: some View { TabView(selection: $selectedTab) { - ScheduleView(presentingAccount: $presentingAccount, activePatientId: $activePatientId) + ScheduleView(presentingAccount: $presentingAccount) .tag(Tabs.schedule) .tabItem { Label("Schedule", systemImage: "list.clipboard") @@ -67,12 +66,16 @@ struct HomeView: View { #endif .autoConnect(enabled: deviceCoordinator.shouldAutoConnectBiopot, with: bluetooth) .onAppear { + guard !ProcessInfo.processInfo.isPreviewSimulator else { + return + } + if FeatureFlags.injectDefaultPatient { Task { let patientId = "default-patient" await patientList.setupTestEnvironment(withPatient: patientId, viewState: $viewState, account: account) - activePatientId = patientId // this will trigger the onChange below, loading the patient info + patientList.activePatientId = patientId // this will trigger the onChange below, loading the patient info handlePatientIdChange() } return @@ -83,7 +86,7 @@ struct HomeView: View { .onDisappear { patientList.removeActivePatientListener() } - .onChange(of: activePatientId, handlePatientIdChange) + .onChange(of: patientList.activePatientId, handlePatientIdChange) .onChange(of: biopot != nil) { guard let biopot else { return @@ -96,12 +99,12 @@ struct HomeView: View { .onChange(of: viewState) { oldValue, newValue in if case .error = oldValue, case .idle = newValue { - activePatientId = nil // reset the current patient on an error + patientList.activePatientId = nil // reset the current patient on an error } } .onChange(of: account.signedIn) { if !account.signedIn { - activePatientId = nil // reset the current patient, will clear model state! + patientList.activePatientId = nil // reset the current patient, will clear model state! } } .sheet(isPresented: $presentingAccount) { @@ -115,8 +118,8 @@ struct HomeView: View { func handlePatientIdChange() { - if let activePatientId { - patientList.loadActivePatient(for: activePatientId, viewState: $viewState, activePatientId: $activePatientId) + if let activePatientId = patientList.activePatientId { + patientList.loadActivePatient(for: activePatientId, viewState: $viewState) } else { patientList.removeActivePatientListener() } diff --git a/NAMS/ObsevableTests.swift b/NAMS/ObsevableTests.swift deleted file mode 100644 index 67d413a..0000000 --- a/NAMS/ObsevableTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ObsevableTests.swift -// NAMS -// -// Created by Andreas Bauer on 06.02.24. -// - -import SwiftUI - -@Observable -class SomeModel { - var counter: Int = 0 - - func task() { - Task { - for _ in 1...1000 { - withMutation(keyPath: \.counter) { - print("changed to \(counter + 1)") - _counter += 1 - // counter += 1 - } - } - } - } -} - -struct ObsevableTests: View { - @State var myModel = SomeModel() - var body: some View { - let ssa = print("Refreshed with \(myModel.counter)") - Text("Counter: \(myModel.counter)") - .onChange(of: myModel.counter) { - print("Changed with \(myModel.counter)") - } - - Button("Start") { - myModel.counter = 0 - myModel.task() - } - } -} - -#Preview { - ObsevableTests() -} diff --git a/NAMS/Patients/CurrentPatientLabel.swift b/NAMS/Patients/CurrentPatientLabel.swift index 5f8228e..7efcc60 100644 --- a/NAMS/Patients/CurrentPatientLabel.swift +++ b/NAMS/Patients/CurrentPatientLabel.swift @@ -13,11 +13,9 @@ struct CurrentPatientLabel: View { @Environment(PatientListModel.self) private var patientList - @Binding private var activePatientId: String? - var body: some View { HStack { - if activePatientId == nil { + if patientList.activePatientId == nil { selectPatientText } else if let patient = patientList.activePatient { Text(verbatim: patient.name.formatted(.name(style: .medium))) @@ -41,16 +39,14 @@ struct CurrentPatientLabel: View { } - init(activePatient: Binding) { - self._activePatientId = activePatient - } + init() {} } #if DEBUG #Preview { Button(action: {}) { - CurrentPatientLabel(activePatient: .constant(nil)) + CurrentPatientLabel() .environment(PatientListModel()) } } diff --git a/NAMS/Patients/Model/PatientListModel.swift b/NAMS/Patients/Model/PatientListModel.swift index a0a5037..075d8a9 100644 --- a/NAMS/Patients/Model/PatientListModel.swift +++ b/NAMS/Patients/Model/PatientListModel.swift @@ -25,6 +25,21 @@ class PatientListModel { var patientList: [Patient]? // swiftlint:disable:this discouraged_optional_collection + @AppStorage(StorageKeys.selectedPatient) + @ObservationIgnored private var _activePatientId: String? + + var activePatientId: String? { + get { + access(keyPath: \.activePatientId) + return _activePatientId + } + set { + withMutation(keyPath: \.activePatientId) { + _activePatientId = newValue + } + } + } + var activePatient: Patient? var completedTasks: [CompletedTask]? // swiftlint:disable:this discouraged_optional_collection @@ -63,7 +78,11 @@ class PatientListModel { patientListListener = patientsCollection .order(by: "name.givenName") .order(by: "name.familyName") - .addSnapshotListener { snapshot, error in + .addSnapshotListener { [weak self] snapshot, error in + guard let self = self else { + return + } + guard let snapshot else { Self.logger.error("Failed to retrieve patient list: \(error)") viewState.wrappedValue = .error(FirestoreError(error!)) // swiftlint:disable:this force_unwrapping @@ -120,13 +139,13 @@ class PatientListModel { } } - func remove(patientId: String, viewState: Binding, activePatientId: Binding) async { + func remove(patientId: String, viewState: Binding) async { if let activePatient, activePatient.id == patientId { removeActivePatientListener() } - if patientId == activePatientId.wrappedValue { - activePatientId.wrappedValue = nil + if patientId == activePatientId { + activePatientId = nil } do { @@ -137,14 +156,18 @@ class PatientListModel { } } - func loadActivePatient(for id: String, viewState: Binding, activePatientId: Binding) { + func loadActivePatient(for id: String, viewState: Binding) { if activePatient?.id == id { return // already set up } removeActivePatientListener() - self.activePatientListener = patientsCollection.document(id).addSnapshotListener { snapshot, error in + self.activePatientListener = patientsCollection.document(id).addSnapshotListener { [weak self] snapshot, error in + guard let self = self else { + return + } + guard let snapshot else { Self.logger.error("Failed to retrieve active patient: \(error)") viewState.wrappedValue = .error(FirestoreError(error!)) // swiftlint:disable:this force_unwrapping @@ -152,7 +175,7 @@ class PatientListModel { } if !snapshot.exists { - activePatientId.wrappedValue = nil + self.activePatientId = nil self.removeActivePatientListener() return } @@ -179,7 +202,11 @@ class PatientListModel { } self.activePatientCompletedTaskListener = completedTasksCollection(patientId: patientId) - .addSnapshotListener { snapshot, error in + .addSnapshotListener { [weak self] snapshot, error in + guard let self = self else { + return + } + guard let snapshot else { Self.logger.error("Failed to retrieve questionnaire responses for active patient: \(error)") viewState.wrappedValue = .error(FirestoreError(error!)) // swiftlint:disable:this force_unwrapping diff --git a/NAMS/Patients/PatientInformation.swift b/NAMS/Patients/PatientInformation.swift index e82a92d..89fd9d3 100644 --- a/NAMS/Patients/PatientInformation.swift +++ b/NAMS/Patients/PatientInformation.swift @@ -23,8 +23,6 @@ struct PatientInformation: View { @State private var viewState: ViewState = .idle @State private var presentingDeleteConfirmation = false - @Binding private var activePatientId: String? - private var name: String { patient.name.formatted(.name(style: .long)) } @@ -63,10 +61,10 @@ struct PatientInformation: View { } @ViewBuilder private var selectButton: some View { - if !patient.isSelectedPatient(active: activePatientId) { + if !patient.isSelectedPatient(active: patientList.activePatientId) { Section { Button(action: { - activePatientId = patient.id + patientList.activePatientId = patient.id dismiss() }) { Text("Select Patient") @@ -94,7 +92,7 @@ struct PatientInformation: View { return } - await patientList.remove(patientId: patientId, viewState: $viewState, activePatientId: $activePatientId) + await patientList.remove(patientId: patientId, viewState: $viewState) dismiss() }) { Text("Delete") @@ -108,9 +106,8 @@ struct PatientInformation: View { } - init(patient: Patient, activePatientId: Binding) { + init(patient: Patient) { self.patient = patient - self._activePatientId = activePatientId } } @@ -118,8 +115,7 @@ struct PatientInformation: View { #if DEBUG #Preview { PatientInformation( - patient: Patient(id: "1234", name: .init(givenName: "Andreas", familyName: "Bauer"), note: "These are some notes ..."), - activePatientId: .constant(nil) + patient: Patient(id: "1234", name: .init(givenName: "Andreas", familyName: "Bauer"), note: "These are some notes ...") ) .environment(PatientListModel()) } diff --git a/NAMS/Patients/PatientList.swift b/NAMS/Patients/PatientList.swift index 4ee3d08..8b13be3 100644 --- a/NAMS/Patients/PatientList.swift +++ b/NAMS/Patients/PatientList.swift @@ -20,7 +20,6 @@ struct PatientList: View { private var searchModel @Binding private var viewState: ViewState - @Binding private var activePatientId: String? private var displayedCount: Int { patients?.reduce(into: 0, { result, element in @@ -40,9 +39,9 @@ struct PatientList: View { } } else { List { - if let selectedPatient = patientList.activePatient, activePatientId != nil { + if let selectedPatient = patientList.activePatient, patientList.activePatientId != nil { Section { - SelectedPatientCard(patient: selectedPatient, activePatientId: $activePatientId) + SelectedPatientCard(patient: selectedPatient) } } @@ -77,10 +76,9 @@ struct PatientList: View { } - init(patients: OrderedDictionary?, viewState: Binding, activePatientId: Binding) { + init(patients: OrderedDictionary?, viewState: Binding) { self.patients = patients self._viewState = viewState - self._activePatientId = activePatientId } @@ -88,7 +86,7 @@ struct PatientList: View { @ViewBuilder func patientRows(_ patients: [Patient]) -> some View { ForEach(patients) { patient in - PatientRow(patient: patient, activePatientId: $activePatientId) + PatientRow(patient: patient) } .onDelete { indexSet in Task { @@ -99,7 +97,7 @@ struct PatientList: View { } - await patientList.remove(patientId: patientId, viewState: $viewState, activePatientId: $activePatientId) + await patientList.remove(patientId: patientId, viewState: $viewState) } } } @@ -121,8 +119,8 @@ struct PatientList: View { ], "P": [Patient(id: "2", name: .init(givenName: "Paul", familyName: "Schmiedmayer"))] ], - viewState: .constant(.idle), - activePatientId: .constant("1") + viewState: .constant(.idle) + // TODO: activePatientId: .constant("1") ) .environment(PatientSearchModel()) .environment(PatientListModel()) @@ -136,21 +134,21 @@ struct PatientList: View { "L": [Patient(id: "3", name: .init(givenName: "Leland", familyName: "Stanford"))], "P": [Patient(id: "2", name: .init(givenName: "Paul", familyName: "Schmiedmayer"))] ], - viewState: .constant(.idle), - activePatientId: .constant("1") + viewState: .constant(.idle) + // TODO: activePatientId: .constant("1") ) .environment(PatientSearchModel()) .environment(PatientListModel()) } } #Preview { - PatientList(patients: [:], viewState: .constant(.idle), activePatientId: .constant(nil)) + PatientList(patients: [:], viewState: .constant(.idle)) .environment(PatientSearchModel()) .environment(PatientListModel()) } #Preview { - PatientList(patients: nil, viewState: .constant(.idle), activePatientId: .constant(nil)) + PatientList(patients: nil, viewState: .constant(.idle)) .environment(PatientSearchModel()) .environment(PatientListModel()) } diff --git a/NAMS/Patients/PatientListSheet.swift b/NAMS/Patients/PatientListSheet.swift index 184eb26..e8b276a 100644 --- a/NAMS/Patients/PatientListSheet.swift +++ b/NAMS/Patients/PatientListSheet.swift @@ -23,15 +23,13 @@ struct PatientListSheet: View { @State private var searchModel = PatientSearchModel() - @Binding private var activePatientId: String? - var body: some View { NavigationStack { - PatientList(patients: patientList.categorizedPatients, viewState: $viewState, activePatientId: $activePatientId) + PatientList(patients: patientList.categorizedPatients, viewState: $viewState) .navigationTitle(Text("Patients", comment: "Patient List Title")) .navigationBarTitleDisplayMode(.inline) .navigationDestination(for: Patient.self) { patient in - PatientInformation(patient: patient, activePatientId: $activePatientId) + PatientInformation(patient: patient) } .environment(searchModel) .sheet(isPresented: $showAddPatientSheet) { @@ -49,6 +47,10 @@ struct PatientListSheet: View { } }) .onAppear { + guard !ProcessInfo.processInfo.isPreviewSimulator else { + return + } + patientList.retrieveList(viewState: $viewState) } .onDisappear { @@ -76,15 +78,13 @@ struct PatientListSheet: View { } - init(activePatientId: Binding) { - self._activePatientId = activePatientId - } + init() {} } #if DEBUG #Preview { - PatientListSheet(activePatientId: .constant(nil)) + PatientListSheet() .environment(PatientListModel()) } #endif diff --git a/NAMS/Patients/PatientRow.swift b/NAMS/Patients/PatientRow.swift index 7ee99c4..39f9f7f 100644 --- a/NAMS/Patients/PatientRow.swift +++ b/NAMS/Patients/PatientRow.swift @@ -15,13 +15,15 @@ import SwiftUI struct PatientRow: View { private let patient: Patient + @Environment(PatientListModel.self) + private var patientList + @Environment(\.dismiss) private var dismiss @Environment(\.editMode) private var editMode @State private var showPatientDetails = false - @Binding private var activePatientId: String? private var patientName: String { patient.name.formatted(.name(style: .long)) @@ -33,12 +35,12 @@ struct PatientRow: View { detailsButton } .navigationDestination(isPresented: $showPatientDetails) { - PatientInformation(patient: patient, activePatientId: $activePatientId) + PatientInformation(patient: patient) } - .accessibilityRepresentation { + .accessibilityRepresentation { @MainActor in Button(action: selectPatientAction) { Text(verbatim: patientName) - if patient.isSelectedPatient(active: activePatientId) { + if patient.isSelectedPatient(active: patientList.activePatientId) { Text("Selected", comment: "Selected Patient") } } @@ -56,7 +58,7 @@ struct PatientRow: View { } } - @ViewBuilder private var selectPatientButton: some View { + @ViewBuilder @MainActor private var selectPatientButton: some View { Button(action: selectPatientAction) { ListRow { HStack { @@ -67,7 +69,7 @@ struct PatientRow: View { } } content: { if editMode?.wrappedValue.isEditing != true - && patient.isSelectedPatient(active: activePatientId) { + && patient.isSelectedPatient(active: patientList.activePatientId) { Text("Selected", comment: "Selected Patient") .foregroundColor(.secondary) } @@ -84,17 +86,17 @@ struct PatientRow: View { } - init(patient: Patient, activePatientId: Binding) { + init(patient: Patient) { self.patient = patient - self._activePatientId = activePatientId } + @MainActor func selectPatientAction() { - if patient.isSelectedPatient(active: activePatientId) { - activePatientId = nil + if patient.isSelectedPatient(active: patientList.activePatientId) { + patientList.activePatientId = nil } else { - activePatientId = patient.id + patientList.activePatientId = patient.id } dismiss() @@ -111,21 +113,21 @@ struct PatientRow: View { NavigationStack { List { PatientRow( - patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")), - activePatientId: .constant(nil) + patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")) ) } } + .environment(PatientListModel()) } #Preview { NavigationStack { List { PatientRow( - patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")), - activePatientId: .constant("1") + patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")) ) } } + .environment(PatientListModel()) } #endif diff --git a/NAMS/Patients/SelectedPatientCard.swift b/NAMS/Patients/SelectedPatientCard.swift index 2527885..d952ec9 100644 --- a/NAMS/Patients/SelectedPatientCard.swift +++ b/NAMS/Patients/SelectedPatientCard.swift @@ -14,15 +14,13 @@ import SwiftUI struct SelectedPatientCard: View { private let patient: Patient - @Binding private var activePatientId: String? - private var name: String { patient.name.formatted(.name(style: .long)) } var body: some View { NavigationLink { - PatientInformation(patient: patient, activePatientId: $activePatientId) + PatientInformation(patient: patient) } label: { HStack { UserProfileView(name: patient.name) @@ -43,9 +41,8 @@ struct SelectedPatientCard: View { } - init(patient: Patient, activePatientId: Binding) { + init(patient: Patient) { self.patient = patient - self._activePatientId = activePatientId } } @@ -55,11 +52,11 @@ struct SelectedPatientCard: View { NavigationStack { List { SelectedPatientCard( - patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")), - activePatientId: .constant("1") + patient: Patient(id: "1", name: .init(givenName: "Andreas", familyName: "Bauer")) ) } .listStyle(.inset) } + .environment(PatientListModel()) } #endif diff --git a/NAMS/Resources/Localizable.xcstrings b/NAMS/Resources/Localizable.xcstrings index e4aec9f..f2887c4 100644 --- a/NAMS/Resources/Localizable.xcstrings +++ b/NAMS/Resources/Localizable.xcstrings @@ -300,9 +300,6 @@ } } } - }, - "Continuous Background Search" : { - }, "Delete" : { "localizations" : { @@ -527,6 +524,12 @@ } } } + }, + "If background search is enabled, the application will search for nearby devices until a devices was found and connected." : { + + }, + "In Background" : { + }, "INTERVENTION_MUSE_FIRMWARE" : { "localizations" : { @@ -780,6 +783,12 @@ } } } + }, + "Off" : { + + }, + "On" : { + }, "Overall" : { "localizations" : { @@ -945,6 +954,9 @@ } } } + }, + "Search in Background" : { + }, "Seconds" : { "localizations" : { @@ -1109,6 +1121,9 @@ } } } + }, + "This device is no longer connected." : { + }, "TP9" : { "localizations" : { @@ -1150,6 +1165,9 @@ } } } + }, + "Unknown Device" : { + }, "WEARING" : { "localizations" : { diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index bf2e8a1..bc23748 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -15,17 +15,17 @@ import SwiftUI struct ScheduleView: View { @Environment(BiopotDevice.self) private var biopot: BiopotDevice? + @Environment(PatientListModel.self) + private var patientList @State private var presentingMuseList = false @State private var presentPatientSheet = false @Binding private var presentingAccount: Bool - @Binding private var activePatientId: String? - var body: some View { NavigationStack { ZStack { - if activePatientId == nil { + if patientList.activePatientId == nil { ContentUnavailableView { Label("No Patient selected", systemImage: "person.fill") } description: { @@ -47,7 +47,7 @@ struct ScheduleView: View { NearbyDevicesView() } .sheet(isPresented: $presentPatientSheet) { - PatientListSheet(activePatientId: $activePatientId) + PatientListSheet() } .toolbar { toolbar @@ -65,11 +65,11 @@ struct ScheduleView: View { } } - ToolbarItem(placement: .principal) { + ToolbarItem(placement: .principal) { // TODO: placement as a ornament on visionOS Button(action: { presentPatientSheet = true }, label: { - CurrentPatientLabel(activePatient: $activePatientId) + CurrentPatientLabel() }) } ToolbarItem(placement: .primaryAction) { @@ -78,16 +78,15 @@ struct ScheduleView: View { } - init(presentingAccount: Binding, activePatientId: Binding) { + init(presentingAccount: Binding) { self._presentingAccount = presentingAccount - self._activePatientId = activePatientId } } #if DEBUG #Preview { - ScheduleView(presentingAccount: .constant(true), activePatientId: .constant(nil)) + ScheduleView(presentingAccount: .constant(true)) .environment(PatientListModel()) .previewWith { EEGRecordings() @@ -102,7 +101,7 @@ struct ScheduleView: View { } #Preview { - ScheduleView(presentingAccount: .constant(true), activePatientId: .constant("1")) + ScheduleView(presentingAccount: .constant(true)) // TODO: set active patient! .environment(PatientListModel()) .previewWith { EEGRecordings() diff --git a/NAMS/Tiles/TilesView.swift b/NAMS/Tiles/TilesView.swift index 9ad857f..d6ce216 100644 --- a/NAMS/Tiles/TilesView.swift +++ b/NAMS/Tiles/TilesView.swift @@ -113,7 +113,7 @@ struct TilesView: View { .environment(patientList) .previewWith { EEGRecordings() - DeviceCoordinator(mock: MockDevice(name: "Mock Device 1", state: .connected)) + DeviceCoordinator(mock: .mock(MockDevice(name: "Mock Device 1", state: .connected))) } } diff --git a/NAMS/Utils/StorageKeys.swift b/NAMS/Utils/StorageKeys.swift index 1b8a771..2c33a36 100644 --- a/NAMS/Utils/StorageKeys.swift +++ b/NAMS/Utils/StorageKeys.swift @@ -22,6 +22,5 @@ enum StorageKeys { static let selectedPatient = "active.patient" // MARK: - Nearby Devices - static let autoConnect = "bluetooth.auto-connect" - static let autoConnectBackground = "bluetooth.auto-connect.background" + static let autoConnectOption = "bluetooth.auto-connect.option" } diff --git a/NAMSTests/BiopotCodingTests.swift b/NAMSTests/BiopotCodingTests.swift index 559dcba..ad8c42e 100644 --- a/NAMSTests/BiopotCodingTests.swift +++ b/NAMSTests/BiopotCodingTests.swift @@ -8,9 +8,9 @@ @testable import NAMS import NIOCore +@_spi(TestingSupport) import SpeziBluetooth -// TODO: @_spi(TestingSupport) import SpeziBluetooth // swiftlint:disable:this attributes -// TODO: import XCTBluetooth +import XCTBluetooth import XCTest @@ -72,7 +72,7 @@ class BiopotCodingTests: XCTestCase { func testRealDeviceConfigurationIdentity() throws { let data = try XCTUnwrap(Data(hex: "0x0000000000080100010918010000007f")) - try testIdentity(of: DeviceConfiguration.self, using: data) + try testIdentity(of: DeviceConfiguration.self, from: data) } func testDataControlDisabled() throws { @@ -82,7 +82,7 @@ class BiopotCodingTests: XCTestCase { let control = try XCTUnwrap(DataControl(from: &buffer)) XCTAssertFalse(control.dataAcquisitionEnabled) - try testIdentity(of: DataControl.self, using: data) + try testIdentity(of: DataControl.self, from: data) } func testDataControlEnabled() throws { @@ -92,7 +92,7 @@ class BiopotCodingTests: XCTestCase { let control = try XCTUnwrap(DataControl(from: &buffer)) XCTAssertTrue(control.dataAcquisitionEnabled) - try testIdentity(of: DataControl.self, using: data) + try testIdentity(of: DataControl.self, from: data) } func testSamplingConfiguration() throws { @@ -111,7 +111,7 @@ class BiopotCodingTests: XCTestCase { // cut of the 2 zero bytes which are not required for proper id check! let idData = try XCTUnwrap(Data(hex: "0x000000ff000701f4090304")) - try testIdentity(of: SamplingConfiguration.self, using: idData) + try testIdentity(of: SamplingConfiguration.self, from: idData) } func testDataAcquisition11_1() throws { @@ -301,64 +301,3 @@ class BiopotCodingTests: XCTestCase { XCTAssertEqual(intLE, -145) } } - - -func testIdentity(of type: T.Type, using data: Data) throws { - var decodingBuffer = ByteBuffer(data: data) - - let instance: T = try XCTUnwrap(T(from: &decodingBuffer)) - - var encodingBuffer = ByteBuffer() - encodingBuffer.reserveCapacity(data.count) - - instance.encode(to: &encodingBuffer) - - let encodingData = Data(buffer: encodingBuffer) - XCTAssertEqual(encodingData, data) -} - - -extension Data { - init?(hex: String) { - // while this seems complicated, and you can do it with shorter code, - // this doesn't incur any heap allocations for string. Pretty neat. - - var index = hex.startIndex - - let hexCount: Int - - if hex.hasPrefix("0x") || hex.hasPrefix("0X") { - index = hex.index(index, offsetBy: 2) - hexCount = hex.count - 2 - } else { - hexCount = hex.count - } - - var bytes: [UInt8] = [] - bytes.reserveCapacity(hexCount / 2 + hexCount % 2) - - if !hexCount.isMultiple(of: 2) { - guard let byte = UInt8(String(hex[index]), radix: 16) else { - return nil - } - bytes.append(byte) - - index = hex.index(after: index) - } - - - while index < hex.endIndex { - guard let byte = UInt8(hex[index ... hex.index(after: index)], radix: 16) else { - return nil - } - bytes.append(byte) - - index = hex.index(index, offsetBy: 2) - } - - guard hexCount / bytes.count == 2 else { - return nil - } - self.init(bytes) - } -} diff --git a/NAMSTests/BiopotDeviceTests.swift b/NAMSTests/BiopotDeviceTests.swift index 2443a4e..252b08b 100644 --- a/NAMSTests/BiopotDeviceTests.swift +++ b/NAMSTests/BiopotDeviceTests.swift @@ -7,22 +7,9 @@ // @testable import NAMS -import Spezi -import SpeziBluetooth -import SwiftUI import XCTest -/* -private class TestDelegate: SpeziAppDelegate { - override var configuration: Configuration { - Configuration { - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(.biopotService)) - } - } - } -} - */ +// TODO: was that file the issue? final class BiopotDeviceTests: XCTestCase { // TODO: How to we test our device test? diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index 321c39d..9475bcc 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -30,18 +30,28 @@ final class BiopotTests: XCTestCase { app.navigationBars.buttons["Nearby Devices"].tap() XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - // TODO: make new tests for basic Biopot funcitonality? - /* - XCTAssertTrue(app.segmentedControls.buttons["Biopot"].exists) - app.segmentedControls.buttons["Biopot"].tap() - - XCTAssertTrue(app.buttons["Receive Device Info"].waitForExistence(timeout: 2.0)) - app.buttons["Receive Device Info"].tap() - - XCTAssertTrue(app.staticTexts["STATUS"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.staticTexts["Battery, 80 %"].exists) - XCTAssertTrue(app.staticTexts["Charging, No"].exists) - XCTAssertTrue(app.staticTexts["Temperature, 23 °C"].exists) - */ + + print(app.buttons.debugDescription) + XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD"].waitForExistence(timeout: 2.0)) + app.buttons["SML BIO 0xAABBCCDD"].tap() + + + XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD, Connected"].waitForExistence(timeout: 2.0)) + // TODO: this is not ideal yet + XCTAssertTrue(app.buttons["Device Details"].waitForExistence(timeout: 2.0)) + app.buttons["Device Details"].tap() + + + XCTAssertTrue(app.navigationBars.staticTexts["SML BIO 0xAABBCCDD"].waitForExistence(timeout: 2.0)) + + XCTAssertTrue(app.staticTexts["Battery, 75 %, is charging"].exists) + XCTAssertTrue(app.staticTexts["Firmware Version, 1.2.3"].exists) + XCTAssertTrue(app.staticTexts["Hardware Version, 3.1"].exists) + XCTAssertTrue(app.staticTexts["Serial Number, 0xAABBCCDD"].exists) + + XCTAssertTrue(app.buttons["Disconnect"].exists) + app.buttons["Disconnect"].tap() + + XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) } } diff --git a/NAMSUITests/MockDeviceTests.swift b/NAMSUITests/MockDeviceTests.swift index cc4dfea..c367a0c 100644 --- a/NAMSUITests/MockDeviceTests.swift +++ b/NAMSUITests/MockDeviceTests.swift @@ -53,17 +53,32 @@ class MockDeviceTests: XCTestCase { XCTAssertTrue(app.navigationBars.staticTexts["Mock Device 1"].waitForExistence(timeout: 2.0)) - XCTAssertTrue(app.staticTexts["Battery, 75 %"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Issues with your battery? Troubleshooting"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["Battery, 75 %"].exists) + XCTAssertTrue(app.staticTexts["Issues with your battery? Troubleshooting"].exists) - XCTAssertTrue(app.staticTexts["HEADBAND"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Wearing, Yes"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Headband Fit, mediocre"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Issues maintaining a good fit? Troubleshooting"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["HEADBAND"].exists) + XCTAssertTrue(app.staticTexts["Wearing, Yes"].exists) + XCTAssertTrue(app.staticTexts["Headband Fit, mediocre"].exists) + XCTAssertTrue(app.staticTexts["Issues maintaining a good fit? Troubleshooting"].exists) - XCTAssertTrue(app.staticTexts["ABOUT"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Serial Number, 0xAABBCCDD"].waitForExistence(timeout: 0.5)) - XCTAssertTrue(app.staticTexts["Firmware Version, 1.2"].waitForExistence(timeout: 0.5)) + XCTAssertTrue(app.staticTexts["ABOUT"].exists) + XCTAssertTrue(app.staticTexts["Serial Number, 0xAABBCCDD"].exists) + XCTAssertTrue(app.staticTexts["Firmware Version, 1.2"].exists) + + XCTAssertTrue(app.buttons["Headband Fit, mediocre"].exists) + app.buttons["Headband Fit, mediocre"].tap() + + // HEADBAND FIT DETAILS + XCTAssertTrue(app.navigationBars.staticTexts["Headband Fit"].waitForExistence(timeout: 2.0)) + + XCTAssertTrue(app.staticTexts["Overall, mediocre"].exists) + XCTAssertTrue(app.staticTexts["TP9, good"].exists) + XCTAssertTrue(app.staticTexts["AF7, mediocre"].exists) + XCTAssertTrue(app.staticTexts["AF8, poor"].exists) + XCTAssertTrue(app.staticTexts["TP10, good"].exists) + + // back button + app.navigationBars.buttons.firstMatch.tap() // DISCONNECT XCTAssertTrue(app.buttons["Disconnect"].waitForExistence(timeout: 0.5)) diff --git a/NAMSUITests/QuestionnaireTests.swift b/NAMSUITests/QuestionnaireTests.swift index a2d9362..4465f70 100644 --- a/NAMSUITests/QuestionnaireTests.swift +++ b/NAMSUITests/QuestionnaireTests.swift @@ -38,25 +38,17 @@ class QuestionnaireTests: XCTestCase { XCTAssertTrue(app.navigationBars.buttons["Cancel"].waitForExistence(timeout: 4)) XCTAssertTrue(app.staticTexts["M-CHAT R/F"].waitForExistence(timeout: 0.5)) - while true { - if app.staticTexts["Yes"].exists { - app.staticTexts["Yes"].tap() - - if app.buttons["Next"].exists { - app.buttons["Next"].tap() - usleep(500_000) - } else if app.buttons["Done"].exists { - app.buttons["Done"].tap() - usleep(500_000) - break - } else { - XCTFail("Couldn't navigate questionnaire!") - } - } else { - XCTFail("Couldn't navigate questionnaire!") - } + XCTAssertTrue(app.staticTexts["Yes"].waitForExistence(timeout: 2.0)) + let yesButtons = app.staticTexts.matching(identifier: "Yes").allElementsBoundByIndex + + for button in yesButtons { + button.tap() + usleep(500_000) } + XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 2.0)) + app.buttons["Done"].tap() + XCTAssertTrue(app.staticTexts["Completed"].waitForExistence(timeout: 2.0)) } From 45a2f87bb7b45e6a42d55f639a41840902593e38 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 17:29:44 -0800 Subject: [PATCH 13/21] Minor adjustments --- NAMS/Devices/DeviceCoordinator.swift | 3 +-- NAMS/Devices/Muse/MuseDeviceManager.swift | 6 ++++++ .../Views/Details/MuseDeviceDetailsView.swift | 2 +- NAMS/EEG/Chart/EEGRecording.swift | 1 - NAMS/EEG/EEGRecordings.swift | 1 - NAMS/Home.swift | 12 ++++++++++-- NAMS/ScheduleView.swift | 15 --------------- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/NAMS/Devices/DeviceCoordinator.swift b/NAMS/Devices/DeviceCoordinator.swift index 6eb5bb5..b76d90d 100644 --- a/NAMS/Devices/DeviceCoordinator.swift +++ b/NAMS/Devices/DeviceCoordinator.swift @@ -74,7 +74,7 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { } @MainActor - func notifyConnectedDevice(_ device: ConnectedDevice) { + func notifyConnectingDevice(_ device: ConnectedDevice) { if let connectedDevice { if connectedDevice != device { logger.info("Nearby device automatically connected, though we already have a connected device. Disconnecting it again...") @@ -98,7 +98,6 @@ class DeviceCoordinator: Module, EnvironmentAccessible, DefaultInitializable { return } logger.debug("Removing association for device disconnecting in background: \(device.label).") - // TODO: deal with auto connecting muse device (after reconnect?) self.connectedDevice = nil } } diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 1fccfae..949785c 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -22,6 +22,12 @@ class MuseDeviceManager { /// The list of nearby muse devices. private(set) var nearbyMuses: [MuseDevice] = [] + var connectedMuse: MuseDevice? { + nearbyMuses.first { muse in + muse.state == .connected + } + } + init() { // TODO: sometimes devices are stale after app open. Look into checking advertising stats or just reconstructing the whole muse manager if nothing is connected upon scanning? self.museManager = IXNMuseManagerIos() diff --git a/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift index 68f219e..055d672 100644 --- a/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift +++ b/NAMS/Devices/Muse/Views/Details/MuseDeviceDetailsView.swift @@ -41,7 +41,7 @@ struct MuseDeviceDetailsView: View { .navigationTitle(Text(verbatim: model)) .navigationBarTitleDisplayMode(.inline) .onChange(of: state) { - if state == .disconnected { // TODO: this doesn't work? + if state == .disconnected { dismiss() } } diff --git a/NAMS/EEG/Chart/EEGRecording.swift b/NAMS/EEG/Chart/EEGRecording.swift index cb0a931..2d1f9e5 100644 --- a/NAMS/EEG/Chart/EEGRecording.swift +++ b/NAMS/EEG/Chart/EEGRecording.swift @@ -73,7 +73,6 @@ struct EEGRecording: View { #endif } .onDisappear { - // TODO: discarding confirmation? Task { try await eegModel.stopRecordingSession() } diff --git a/NAMS/EEG/EEGRecordings.swift b/NAMS/EEG/EEGRecordings.swift index 9e2a0db..21be331 100644 --- a/NAMS/EEG/EEGRecordings.swift +++ b/NAMS/EEG/EEGRecordings.swift @@ -47,7 +47,6 @@ class EEGRecordings: Module, EnvironmentAccessible, DefaultInitializable { throw EEGRecordingError.noConnectedDevice } - // TODO: handle the case where the device disconnects when an ongoing recording is in progress? => Issue try await device.startRecording(session) // We set the recording session after recording was enabled on the device. diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 2be4f27..08ae9d8 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -91,11 +91,19 @@ struct HomeView: View { guard let biopot else { return } - // TODO: we kinda also need connecting state!? just change Bluetooth behavior? // a new device is connected now - deviceCoordinator.notifyConnectedDevice(.biopot(biopot)) + deviceCoordinator.notifyConnectingDevice(.biopot(biopot)) } +#if MUSE + .onChange(of: museDeviceManager.connectedMuse) { _, muse in + guard let muse else { + return + } + + deviceCoordinator.notifyConnectingDevice(.muse(muse)) + } +#endif .onChange(of: viewState) { oldValue, newValue in if case .error = oldValue, case .idle = newValue { diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index bc23748..f74f754 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -99,19 +99,4 @@ struct ScheduleView: View { } } } - -#Preview { - ScheduleView(presentingAccount: .constant(true)) // TODO: set active patient! - .environment(PatientListModel()) - .previewWith { - EEGRecordings() - DeviceCoordinator() - Bluetooth { - Discover(BiopotDevice.self, by: .advertisedService(BiopotService.self)) - } - AccountConfiguration { - MockUserIdPasswordAccountService() - } - } -} #endif From e88b5e7d0754ec4c74e4702545b06728cac6cf9f Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 18:57:54 -0800 Subject: [PATCH 14/21] Fix stale muse devices when bluetooth is turned off or scanning is stopped for other reasons --- NAMS copy-Info.plist | 19 --- NAMS.xcodeproj/project.pbxproj | 2 - NAMS/Devices/Muse/MuseDevice.swift | 8 +- NAMS/Devices/Muse/MuseDeviceManager.swift | 139 ++++++++++++++++++++-- NAMS/Devices/NearbyDevicesView.swift | 5 +- NAMS/Patients/PatientList.swift | 2 - NAMS/ScheduleView.swift | 2 +- 7 files changed, 138 insertions(+), 39 deletions(-) delete mode 100644 NAMS copy-Info.plist diff --git a/NAMS copy-Info.plist b/NAMS copy-Info.plist deleted file mode 100644 index 1b53361..0000000 --- a/NAMS copy-Info.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - ITSAppUsesNonExemptEncryption - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - - UISupportedExternalAccessoryProtocols - - com.interaxon.muse - - - diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index d604a51..be5a869 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -396,7 +396,6 @@ A94A42AD2AE9EBE300A3F9E5 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A94A42B92AE9ED8200A3F9E5 /* AccountOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; A967061B2B1AA2E000C17BE5 /* EEGRecordingSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EEGRecordingSession.swift; sourceTree = ""; }; - A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "NAMS copy-Info.plist"; path = "/Users/andi/XcodeProjects/Stanford/NAMS/NAMS copy-Info.plist"; sourceTree = ""; }; A97E4F1E2B1EA0D600E25505 /* StartRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartRecordingView.swift; sourceTree = ""; }; A97E4F222B1EA21800E25505 /* EEGChannel+Biopot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EEGChannel+Biopot.swift"; sourceTree = ""; }; A988FEA92B0414FD00022A61 /* BiopotDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiopotDevice.swift; sourceTree = ""; }; @@ -571,7 +570,6 @@ 653A256028338800005D4D48 /* NAMSTests */, 653A256A28338800005D4D48 /* NAMSUITests */, 653A254E283387FE005D4D48 /* Products */, - A97A99E12B6CB8680021D80A /* NAMS copy-Info.plist */, ); sourceTree = ""; }; diff --git a/NAMS/Devices/Muse/MuseDevice.swift b/NAMS/Devices/Muse/MuseDevice.swift index 7567144..5011141 100644 --- a/NAMS/Devices/Muse/MuseDevice.swift +++ b/NAMS/Devices/Muse/MuseDevice.swift @@ -101,8 +101,12 @@ class MuseDevice: Identifiable { muse.getRssi() } - var lastDiscoveredTime: Double { - muse.getLastDiscoveredTime() + var lastDiscoveredTime: Double? { + let time = muse.getLastDiscoveredTime() + guard !time.isNaN else { + return nil + } + return time } var underlyingDevice: IXNMuse { diff --git a/NAMS/Devices/Muse/MuseDeviceManager.swift b/NAMS/Devices/Muse/MuseDeviceManager.swift index 949785c..30e6e4d 100644 --- a/NAMS/Devices/Muse/MuseDeviceManager.swift +++ b/NAMS/Devices/Muse/MuseDeviceManager.swift @@ -9,11 +9,13 @@ import Observation import OSLog import SpeziBluetooth +import SwiftUI #if MUSE @Observable class MuseDeviceManager { + private static let discoveryTimeout: Int64 = 10 private let logger = Logger(subsystem: "edu.stanford.NAMS", category: "MuseDeviceManager") private let museManager: IXNMuseManager @@ -21,15 +23,25 @@ class MuseDeviceManager { /// The list of nearby muse devices. private(set) var nearbyMuses: [MuseDevice] = [] + /// When the app gets to sleep Muse doesn't continue to count down their stale timer and doesn't remove devices. + /// Therefore, we hide them base on the last seen time. However, we don't get notified when the device is suddenly discoverable again. + /// So we will actively poll hidden devices if we have some. + private var hiddenMuseDevices: Set = [] + private var hiddenDevicesTimer: Timer? { + willSet { + hiddenDevicesTimer?.invalidate() + } + } + + private var lastKnownBluetoothState: BluetoothState = .unknown - var connectedMuse: MuseDevice? { + @MainActor var connectedMuse: MuseDevice? { nearbyMuses.first { muse in muse.state == .connected } } init() { - // TODO: sometimes devices are stale after app open. Look into checking advertising stats or just reconstructing the whole muse manager if nothing is connected upon scanning? self.museManager = IXNMuseManagerIos() self.museListener = MuseListener(deviceManager: self) @@ -37,25 +49,90 @@ class MuseDeviceManager { logger.debug("Initialized Muse Manager with API version \(apiVersion.getString())") } - self.museManager.removeFromList(after: 10) // stale timeout if there isn't an updated advertisement + self.museManager.removeFromList(after: Self.discoveryTimeout) // stale timeout if there isn't an updated advertisement } + @MainActor func startScanning() { logger.debug("Start scanning for nearby Muse devices...") + if lastKnownBluetoothState == .poweredOff { + self.museManager.stopListening() // make sure it is balanced + } + + self.lastKnownBluetoothState = .poweredOn self.museManager.startListening() + handleUpdatedDeviceList(museManager.getMuses()) } - func stopScanning() { + @MainActor + func stopScanning(state: BluetoothState) { logger.debug("Stopped scanning for nearby Muse devices!") - self.museManager.stopListening() + self.lastKnownBluetoothState = state + if state == .poweredOn { + self.museManager.stopListening() + } + + if state != .poweredOn { + // MuseManager stops its stale timer once we stop listening. + // If we are stopping because Bluetooth turned off, we just assume all devices to be hidden. + for device in nearbyMuses { + logger.debug("\(device.label) is considered hidden as bluetooth was disabled.") + hiddenMuseDevices.insert(device.underlyingDevice) + } + nearbyMuses.removeAll() + + checkHiddenTimerScheduled() + } } + @MainActor + func stopScanning() { + // we are called from the modifier, so state must be powered on + stopScanning(state: .poweredOn) + } + + private func isHiddenDevice(_ muse: IXNMuse) -> Bool { + if lastKnownBluetoothState == .poweredOff { + return true // we consider all devices hidden when bluetooth is off + } + + let lastTime = muse.getLastDiscoveredTime() + guard !lastTime.isNaN || muse.getConnectionState() == .disconnected else { + return false // just accept those that don't expose a time + } + + // that's how muse calculates the discovered time + let now = CACurrentMediaTime() * 1000.0 * 1000.0 + + let delta = max(0, now - lastTime) + + return delta > (Double(Self.discoveryTimeout) * 1000.0 * 1000.0) + } - private func handleUpdatedDeviceList() { - let nearbyMuses = museManager.getMuses() + + @MainActor + private func handleUpdatedDeviceList(_ museList: [IXNMuse]) { + MainActor.assertIsolated("Muse List was not updated on Main Actor!") + var nearbyMuses = Set(museList) + + // check if a hidden muse is gone now + for muse in hiddenMuseDevices where !nearbyMuses.contains(muse) { + hiddenMuseDevices.remove(muse) + } + + // check if muse is hidden or a hidden one is not hidden anymore? + for (index, muse) in zip(nearbyMuses.indices, nearbyMuses).reversed() { + if isHiddenDevice(muse) { + hiddenMuseDevices.insert(muse) + nearbyMuses.remove(at: index) + logger.debug("\(muse.getModel()) - \(muse.getName()) is stale and we are hiding it.") + } else { + hiddenMuseDevices.remove(muse) + } + } // remove all muses that went away - for (index, removedMuse) in self.nearbyMuses.enumerated() { + for (index, removedMuse) in zip(self.nearbyMuses.indices, self.nearbyMuses).reversed() { guard !nearbyMuses.contains(removedMuse.underlyingDevice) else { continue } @@ -68,6 +145,43 @@ class MuseDeviceManager { } self.nearbyMuses.append(MuseDevice(addedMuse)) } + + checkHiddenTimerScheduled() + } + + @MainActor + private func checkHiddenMuseDevices() { + var hiddenDeviceUpdated = false + + for device in hiddenMuseDevices where !isHiddenDevice(device) { + // device updated again + logger.debug("\(device.getModel()) - \(device.getName()) is not hidden anymore!") + hiddenDeviceUpdated = true + break + } + + if hiddenDeviceUpdated { + hiddenDevicesTimer = nil + handleUpdatedDeviceList(museManager.getMuses()) + } + } + + @MainActor + private func checkHiddenTimerScheduled() { + if hiddenMuseDevices.isEmpty { + hiddenDevicesTimer = nil + } else if hiddenDevicesTimer == nil { + let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self = self else { + return + } + MainActor.assumeIsolated { + self.checkHiddenMuseDevices() + } + } + hiddenDevicesTimer = timer + RunLoop.main.add(timer, forMode: .common) + } } deinit { @@ -85,7 +199,7 @@ extension MuseDeviceManager: BluetoothScanner { func scanNearbyDevices(autoConnect: Bool) async { precondition(!autoConnect, "AutoConnect is unsupported on \(Self.self)") - self.startScanning() + await self.startScanning() } func setAutoConnect(_ autoConnect: Bool) async { @@ -109,7 +223,12 @@ extension MuseDeviceManager { guard let deviceManager else { return } - deviceManager.handleUpdatedDeviceList() + + let museList = deviceManager.museManager.getMuses() + + Task { @MainActor in + deviceManager.handleUpdatedDeviceList(museList) + } } deinit { diff --git a/NAMS/Devices/NearbyDevicesView.swift b/NAMS/Devices/NearbyDevicesView.swift index 7f1a820..b860363 100644 --- a/NAMS/Devices/NearbyDevicesView.swift +++ b/NAMS/Devices/NearbyDevicesView.swift @@ -75,13 +75,12 @@ struct NearbyDevicesView: View { .scanNearbyDevices(with: bluetooth, autoConnect: deviceCoordinator.shouldAutoConnectBiopot) .scanNearbyDevices(enabled: mockDeviceManager != nil, with: mockDeviceManager ?? MockDeviceManager()) #if MUSE - .scanNearbyDevices(enabled: bluetooth.state == .poweredOn, with: museDeviceManager) + .scanNearbyDevices(with: museDeviceManager) .onChange(of: bluetooth.state) { if case .poweredOn = bluetooth.state { museDeviceManager.startScanning() } else { - // this will still trigger an API MISUSE, but otherwise we end up in undefined state - // TODO: museDeviceManager.stopScanning() + museDeviceManager.stopScanning(state: bluetooth.state) } } #endif diff --git a/NAMS/Patients/PatientList.swift b/NAMS/Patients/PatientList.swift index 8b13be3..edee4af 100644 --- a/NAMS/Patients/PatientList.swift +++ b/NAMS/Patients/PatientList.swift @@ -120,7 +120,6 @@ struct PatientList: View { "P": [Patient(id: "2", name: .init(givenName: "Paul", familyName: "Schmiedmayer"))] ], viewState: .constant(.idle) - // TODO: activePatientId: .constant("1") ) .environment(PatientSearchModel()) .environment(PatientListModel()) @@ -135,7 +134,6 @@ struct PatientList: View { "P": [Patient(id: "2", name: .init(givenName: "Paul", familyName: "Schmiedmayer"))] ], viewState: .constant(.idle) - // TODO: activePatientId: .constant("1") ) .environment(PatientSearchModel()) .environment(PatientListModel()) diff --git a/NAMS/ScheduleView.swift b/NAMS/ScheduleView.swift index f74f754..831d9eb 100644 --- a/NAMS/ScheduleView.swift +++ b/NAMS/ScheduleView.swift @@ -65,7 +65,7 @@ struct ScheduleView: View { } } - ToolbarItem(placement: .principal) { // TODO: placement as a ornament on visionOS + ToolbarItem(placement: .principal) { Button(action: { presentPatientSheet = true }, label: { From ac9ff945ff9305c451109773487d6650a598f178 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 23:29:49 -0800 Subject: [PATCH 15/21] Fix tests --- .swiftlint.yml | 1 + NAMS.xcodeproj/project.pbxproj | 2 +- NAMS/Devices/Biopot/BiopotDevice.swift | 4 +- NAMS/Home.swift | 2 + NAMS/Patients/PatientRow.swift | 25 +++++---- NAMSTests/BiopotDeviceTests.swift | 70 +++++++++++++++++--------- NAMSUITests/OnboardingTests.swift | 2 - 7 files changed, 63 insertions(+), 43 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 72c79de..e8f1303 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -376,6 +376,7 @@ deployment_target: # Availability checks or attributes shouldn’t be using olde excluded: # paths to ignore during linting. Takes precedence over `included`. - .build + - .idea - .swiftpm - .codeql - .derivedData diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index be5a869..cfffb3c 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -2242,7 +2242,7 @@ repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.4.7; + minimumVersion = 0.4.8; }; }; A926D7662AB7A552000C4C2F /* XCRemoteSwiftPackageReference "Spezi" */ = { diff --git a/NAMS/Devices/Biopot/BiopotDevice.swift b/NAMS/Devices/Biopot/BiopotDevice.swift index a403a51..8654f83 100644 --- a/NAMS/Devices/Biopot/BiopotDevice.swift +++ b/NAMS/Devices/Biopot/BiopotDevice.swift @@ -115,7 +115,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { } @MainActor - func enableRecording() async throws { + private func enableRecording() async throws { do { try await service.$dataControl.write(false) @@ -132,7 +132,7 @@ class BiopotDevice: BluetoothDevice, Identifiable { } } - private func handleDataAcquisition(data: Data) { + func handleDataAcquisition(data: Data) { guard let deviceConfiguration = service.deviceConfiguration else { logger.debug("Received data acquisition without having device configuration ready!") return diff --git a/NAMS/Home.swift b/NAMS/Home.swift index 08ae9d8..6cacd48 100644 --- a/NAMS/Home.swift +++ b/NAMS/Home.swift @@ -34,6 +34,8 @@ struct HomeView: View { #if TEST || DEBUG @State var mockDeviceManager = MockDeviceManager() +#else + @State var mockDeviceManager: MockDeviceManager? #endif #if MUSE diff --git a/NAMS/Patients/PatientRow.swift b/NAMS/Patients/PatientRow.swift index 39f9f7f..117a8b2 100644 --- a/NAMS/Patients/PatientRow.swift +++ b/NAMS/Patients/PatientRow.swift @@ -38,22 +38,21 @@ struct PatientRow: View { PatientInformation(patient: patient) } .accessibilityRepresentation { @MainActor in - Button(action: selectPatientAction) { - Text(verbatim: patientName) - if patient.isSelectedPatient(active: patientList.activePatientId) { - Text("Selected", comment: "Selected Patient") + HStack { @MainActor in + Button(action: selectPatientAction) { + Text(verbatim: patientName) + Spacer() + if patient.isSelectedPatient(active: patientList.activePatientId) { + Text("Selected", comment: "Selected Patient") + } } +#if TEST + detailsButton + .accessibilityLabel("\(patientName), Patient Details") +#endif } #if !TEST - .accessibilityAction(named: "Patient Details", detailsButtonAction) -#else - // accessibility actions cannot be unit tested - .frame(maxWidth: .infinity) -#endif - -#if TEST - detailsButton - .accessibilityLabel("\(patientName), Patient Details") + .accessibilityAction(named: "Patient Details", detailsButtonAction) #endif } } diff --git a/NAMSTests/BiopotDeviceTests.swift b/NAMSTests/BiopotDeviceTests.swift index 252b08b..48f7bcf 100644 --- a/NAMSTests/BiopotDeviceTests.swift +++ b/NAMSTests/BiopotDeviceTests.swift @@ -7,42 +7,62 @@ // @testable import NAMS +@_spi(TestingSupport) +import SpeziBluetooth import XCTest -// TODO: was that file the issue? final class BiopotDeviceTests: XCTestCase { - // TODO: How to we test our device test? - - /* private var device: BiopotDevice! // swiftlint:disable:this implicitly_unwrapped_optional override func setUpWithError() throws { - let device = BiopotDevice() + self.device = BiopotDevice.createMock() + } - // this is a workaround to call the Spezi initializer here, to inject all dependencies. - _ = EmptyView() - .spezi(TestDelegate(device: device)) + func testDataAcquisition() async throws { + let data = try XCTUnwrap(Data( + hex: """ + 0x1b000000\ + 6aab9f6aab9f6aab9f6aab9f6aab9f6aab9f6aab9f6aab9f\ + 1e35a11e35a11e35a11e35a11e35a11e35a11e35a11e35a1\ + 75c6a275c6a275c6a275c6a275c6a275c6a275c6a275c6a2\ + ed5ba4ed5ba4ed5ba4ed5ba4ed5ba4ed5ba4ed5ba4ed5ba4\ + d8f2a5d8f2a5d8f2a5d8f2a5d8f2a5d8f2a5d8f2a5d8f2a5\ + 2889a72889a72889a72889a72889a72889a72889a72889a7\ + 4e1da94e1da94e1da94e1da94e1da94e1da94e1da94e1da9\ + 1eaeaa1eaeaa1eaeaa1eaeaa1eaeaa1eaeaa1eaeaa1eaeaa\ + b63aacb63aacb63aacb63aacb63aacb63aacb63aacb63aac\ + 87ffff87ffff87ffff87ffff87ffff87ffff87ffff87ffff + """ + )) - self.device = device - } + let configuration = DeviceConfiguration( + channelCount: 8, + accelerometerStatus: .off, + impedanceStatus: false, + memoryStatus: false, + samplesPerChannel: 9, + dataSize: 24, + syncEnabled: false, + serialNumber: 127 + ) - @MainActor - func testReceiveDeviceInformation() async throws { - let data = try XCTUnwrap(Data(hex: "0x000000000000000000000001561c010000000000")) - await device.recieve(data, service: BiopotDevice.Service.biopot, characteristic: BiopotDevice.Characteristic.biopotDeviceInfo) + device.service.$deviceConfiguration.inject(configuration) + device.service.$dataAcquisition.inject(data) - let expected = DeviceInformation( - syncRatio: 0, - syncMode: false, - memoryWriteNumber: 0, - memoryEraseMode: true, - batteryLevel: 86, - temperatureValue: 28, - batteryCharging: false - ) + let session = EEGRecordingSession() + + do { + try await device.startRecording(session) + } catch { + // this will throw because there is no peripheral connected, but we only care about assigning the session + } - XCTAssertEqual(device.deviceInfo, expected) - }*/ + device.handleDataAcquisition(data: data) + + try await Task.sleep(for: .milliseconds(100)) + print(session.measurements) + // TODO: assert measurements + } } diff --git a/NAMSUITests/OnboardingTests.swift b/NAMSUITests/OnboardingTests.swift index 5e1aeaf..005f3ab 100644 --- a/NAMSUITests/OnboardingTests.swift +++ b/NAMSUITests/OnboardingTests.swift @@ -12,8 +12,6 @@ import XCTestExtensions class OnboardingTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - - try disablePasswordAutofill() sleep(1) From 8e817e133e9ec33053b49619770446b4845b5e87 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Fri, 9 Feb 2024 23:41:05 -0800 Subject: [PATCH 16/21] Resolve todos --- NAMSTests/BiopotDeviceTests.swift | 10 ++++++++-- NAMSUITests/BiopotTests.swift | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/NAMSTests/BiopotDeviceTests.swift b/NAMSTests/BiopotDeviceTests.swift index 48f7bcf..a390ded 100644 --- a/NAMSTests/BiopotDeviceTests.swift +++ b/NAMSTests/BiopotDeviceTests.swift @@ -62,7 +62,13 @@ final class BiopotDeviceTests: XCTestCase { device.handleDataAcquisition(data: data) try await Task.sleep(for: .milliseconds(100)) - print(session.measurements) - // TODO: assert measurements + + XCTAssertEqual(session.measurements.count, 1) + + let series = try XCTUnwrap(session.measurements[.all]) + XCTAssertEqual(series.count, 10) + + let channels = try XCTUnwrap(series.first) + XCTAssertEqual(channels.channels.count, 8) } } diff --git a/NAMSUITests/BiopotTests.swift b/NAMSUITests/BiopotTests.swift index 9475bcc..8f54fc0 100644 --- a/NAMSUITests/BiopotTests.swift +++ b/NAMSUITests/BiopotTests.swift @@ -31,13 +31,11 @@ final class BiopotTests: XCTestCase { XCTAssertTrue(app.navigationBars.staticTexts["Nearby Devices"].waitForExistence(timeout: 2.0)) - print(app.buttons.debugDescription) XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD"].waitForExistence(timeout: 2.0)) app.buttons["SML BIO 0xAABBCCDD"].tap() XCTAssertTrue(app.buttons["SML BIO 0xAABBCCDD, Connected"].waitForExistence(timeout: 2.0)) - // TODO: this is not ideal yet XCTAssertTrue(app.buttons["Device Details"].waitForExistence(timeout: 2.0)) app.buttons["Device Details"].tap() From 39249feb3663377a00803a5c6c9a06544bba6162 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sat, 10 Feb 2024 00:03:05 -0800 Subject: [PATCH 17/21] Disable password autofill via command line --- .github/workflows/build-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2807cb4..8cddb75 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -27,6 +27,7 @@ jobs: artifactname: NAMS.xcresult runsonlabels: '["macOS", "self-hosted"]' setupfirebaseemulator: true + setupSimulators: true customcommand: "firebase emulators:exec 'fastlane test'" releasebuild: name: Build Release From b0ec00057ec80e8eb919dbfbab3cbea75b1f2626 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sat, 10 Feb 2024 00:16:38 -0800 Subject: [PATCH 18/21] Do not reset simulators in fastlane? --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 20b6a2c..846352f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,7 +23,7 @@ platform :ios do code_coverage: true, devices: ["iPhone 15 Pro"], force_quit_simulator: true, - reset_simulator: true, + reset_simulator: false, prelaunch_simulator: false, concurrent_workers: 1, max_concurrent_simulators: 1, From 7224746d149e1fdf9ed056ebef8cc3d6dc74f5ef Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sat, 10 Feb 2024 00:46:12 -0800 Subject: [PATCH 19/21] Replicate template app setup --- NAMS.xcodeproj/project.pbxproj | 18 +++--------------- fastlane/Fastfile | 4 +--- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index cfffb3c..86797ef 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -1490,7 +1490,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -1547,7 +1547,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -1596,7 +1596,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1657,7 +1656,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1688,7 +1686,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplication.tests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1708,7 +1705,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplication.tests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1727,7 +1723,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplicationuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1746,7 +1741,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplicationuitests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1798,7 +1792,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1860,7 +1853,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1935,7 +1927,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -1984,7 +1976,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2047,7 +2038,6 @@ INFOPLIST_KEY_UINAMSlicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2079,7 +2069,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplication.tests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2098,7 +2087,6 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.templateapplicationuitests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 846352f..421106f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -22,9 +22,7 @@ platform :ios do derived_data_path: ".derivedData", code_coverage: true, devices: ["iPhone 15 Pro"], - force_quit_simulator: true, - reset_simulator: false, - prelaunch_simulator: false, + disable_slide_to_type: false, concurrent_workers: 1, max_concurrent_simulators: 1, result_bundle: true, From ebe0f68d2178e9ef9ad91be0c1985073fc86eca5 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sat, 10 Feb 2024 16:19:25 -0800 Subject: [PATCH 20/21] Minor changes --- .github/workflows/beta-deployment.yml | 8 ++++++-- .github/workflows/build-and-test.yml | 17 +++++++++++++++++ fastlane/Fastfile | 7 +++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/beta-deployment.yml b/.github/workflows/beta-deployment.yml index 1bc6221..9c4b28e 100644 --- a/.github/workflows/beta-deployment.yml +++ b/.github/workflows/beta-deployment.yml @@ -1,5 +1,5 @@ # -# This source file is part of the Neurodevelopment Assessment and Monitoring System (NAMS) project +# This source file is part of the Stanford Spezi Template Application open-source project # # SPDX-FileCopyrightText: 2023 Stanford University # @@ -18,15 +18,19 @@ jobs: buildandtest: name: Build and Test uses: ./.github/workflows/build-and-test.yml + permissions: + contents: read secrets: inherit iosapptestflightdeployment: name: iOS App TestFlight Deployment needs: buildandtest uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + permissions: + contents: read secrets: inherit with: artifactname: NAMS.xcresult - runsonlabels: '["macOS-13"]' + runsonlabels: '["macOS", "self-hosted"]' fastlanelane: beta setupsigning: true checkout_submodules: true diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8cddb75..da697c6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,12 +17,23 @@ jobs: reuse_action: name: REUSE Compliance Check uses: StanfordBDHG/.github/.github/workflows/reuse.yml@v2 + permissions: + contents: read swiftlint: name: SwiftLint uses: StanfordBDHG/.github/.github/workflows/swiftlint.yml@v2 + permissions: + contents: read + markdownlinkcheck: + name: Markdown Link Check + uses: StanfordBDHG/.github/.github/workflows/markdown-link-check.yml@v2 + permissions: + contents: read buildandtest: name: Build and Test uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + permissions: + contents: read with: artifactname: NAMS.xcresult runsonlabels: '["macOS", "self-hosted"]' @@ -32,6 +43,8 @@ jobs: releasebuild: name: Build Release uses: StanfordBDHG/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + permissions: + contents: read secrets: inherit with: runsonlabels: '["macOS", "self-hosted"]' @@ -42,5 +55,9 @@ jobs: name: Upload Coverage Report needs: buildandtest uses: StanfordBDHG/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 + permissions: + contents: read with: coveragereports: NAMS.xcresult + secrets: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 421106f..c2b925e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -26,7 +26,8 @@ platform :ios do concurrent_workers: 1, max_concurrent_simulators: 1, result_bundle: true, - output_directory: "." + output_directory: ".", + xcargs: "-skipPackagePluginValidation" ) end @@ -36,7 +37,8 @@ platform :ios do scheme: "NAMS", skip_archive: true, skip_codesigning: true, - derived_data_path: ".derivedData" + derived_data_path: ".derivedData", + xcargs: "-skipPackagePluginValidation" ) end @@ -48,6 +50,7 @@ platform :ios do output_directory: ".build", archive_path: ".build/NAMS.xcarchive", configuration: "Release", + xcargs: "-skipPackagePluginValidation", export_options: { method: "app-store", signingStyle: "manual", From 2fe1b4232c8446b559906d850c3c472ec8d72984 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Thu, 22 Feb 2024 11:34:30 -0800 Subject: [PATCH 21/21] Upgrade to release versions --- NAMS.xcodeproj/project.pbxproj | 8 ++++---- .../xcshareddata/swiftpm/Package.resolved | 20 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/NAMS.xcodeproj/project.pbxproj b/NAMS.xcodeproj/project.pbxproj index 86797ef..e64ef7c 100644 --- a/NAMS.xcodeproj/project.pbxproj +++ b/NAMS.xcodeproj/project.pbxproj @@ -2189,8 +2189,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziOnboarding.git"; requirement = { - branch = "feature/vision-os"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; }; }; 2FE5DC8229EDD934004B9AB4 /* XCRemoteSwiftPackageReference "SpeziQuestionnaire" */ = { @@ -2309,8 +2309,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordSpezi/SpeziBluetooth"; requirement = { - branch = "feature/unit-testing-setup"; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4578558..69a8565 100644 --- a/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NAMS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/Spezi", "state" : { - "revision" : "c4bf0e99de40acfdd2baf0fa02769f06a4c3f0eb", - "version" : "1.1.0" + "revision" : "c8482d05efbd2ba93abefc309a86f7b77d10fca0", + "version" : "1.2.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziBluetooth", "state" : { - "branch" : "feature/unit-testing-setup", - "revision" : "16ce491c3f24c297ff55351c04bd87402af1a7bf" + "revision" : "b8e0a7fdc9c20175e024daa8288471d8bcba5b22", + "version" : "1.0.0" } }, { @@ -194,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziOnboarding.git", "state" : { - "branch" : "feature/vision-os", - "revision" : "a11d24e8917c11c1bc226eb970a7419047a035f7" + "revision" : "91463ae190611bd14ef52b0657e8db3bf53c9ae8", + "version" : "1.1.0" } }, { @@ -221,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordSpezi/SpeziViews.git", "state" : { - "revision" : "7210f72d6821d2eeb93438b29cb854a8ce334164", - "version" : "1.2.0" + "revision" : "d49f716e4a4d634604bb0dcd6d53df679b6c1358", + "version" : "1.3.0" } }, { @@ -284,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", "state" : { - "revision" : "bb2a287c2544aa846e53670d1ece35e5949567be", - "version" : "1.0.0" + "revision" : "51da3403f128b120705571ce61e0fe190f8889e6", + "version" : "1.0.1" } } ],