summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArturs Artamonovs <arturs.artamonovs@protonmail.com>2025-01-17 09:27:56 +0000
committerArturs Artamonovs <arturs.artamonovs@protonmail.com>2025-01-17 09:27:56 +0000
commit06a90001363fdcb542b531a74ed2c18f714f58c4 (patch)
treee0ae3092494e6136ec7479b0e9bda20ef930039b
parenta0d12ecbac8fe327d1dcd4580fee594e24d4191b (diff)
downloadPrySDR-06a90001363fdcb542b531a74ed2c18f714f58c4.tar.gz
PrySDR-06a90001363fdcb542b531a74ed2c18f714f58c4.zip
waterfallui: data get processed and drawn, ui controlls are dummy
-rw-r--r--PrySDR.xcodeproj/project.pbxproj377
-rw-r--r--Radio/Utils/WaterfallFile/NaiveFFT512.swift24
-rw-r--r--WaterfallFile_UI/ContentView.swift1
-rw-r--r--WaterfallFile_UI/FileSpectrum.swift2
-rw-r--r--Waterfall_UI/Assets.xcassets/AccentColor.colorset/Contents.json11
-rw-r--r--Waterfall_UI/Assets.xcassets/AppIcon.appiconset/Contents.json58
-rw-r--r--Waterfall_UI/Assets.xcassets/Contents.json6
-rw-r--r--Waterfall_UI/ContentView.swift194
-rw-r--r--Waterfall_UI/Item.swift18
-rw-r--r--Waterfall_UI/Preview Content/Preview Assets.xcassets/Contents.json6
-rw-r--r--Waterfall_UI/SDRSpectrum.swift137
-rw-r--r--Waterfall_UI/Waterfall_UI.entitlements12
-rw-r--r--Waterfall_UI/Waterfall_UIApp.swift46
-rw-r--r--Waterfall_UITests/Waterfall_UITests.swift17
-rw-r--r--Waterfall_UIUITests/Waterfall_UIUITests.swift43
-rw-r--r--Waterfall_UIUITests/Waterfall_UIUITestsLaunchTests.swift33
16 files changed, 984 insertions, 1 deletions
diff --git a/PrySDR.xcodeproj/project.pbxproj b/PrySDR.xcodeproj/project.pbxproj
index 72c61bf..817e90c 100644
--- a/PrySDR.xcodeproj/project.pbxproj
+++ b/PrySDR.xcodeproj/project.pbxproj
@@ -68,6 +68,9 @@
8D9A33502D0B0DD8009A4186 /* libbladerf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D87709D2CD6B4BB0082EC54 /* libbladerf.a */; };
8D9A33522D0B0DE5009A4186 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8D9A33512D0B0DE5009A4186 /* ArgumentParser */; };
8D9A38852D19D427009A4186 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8D9A38842D19D427009A4186 /* ArgumentParser */; };
+ 8DBA9FA22D37C5C8008ECB92 /* libr820.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D5A5DB42CD4B70D0096CBD7 /* libr820.a */; };
+ 8DBA9FA32D37C5CD008ECB92 /* libusb.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D5A5DDA2CD4B9100096CBD7 /* libusb.a */; };
+ 8DBA9FA52D37C5D1008ECB92 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 8DBA9FA42D37C5D1008ECB92 /* ArgumentParser */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -239,6 +242,20 @@
remoteGlobalIDString = 8D9A38762D196510009A4186;
remoteInfo = WaterfallFile;
};
+ 8DBA9F822D2C979A008ECB92 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 8DD98C392CC592540062D678 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 8DBA9F6E2D2C9796008ECB92;
+ remoteInfo = Waterfall_UI;
+ };
+ 8DBA9F8C2D2C979A008ECB92 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 8DD98C392CC592540062D678 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 8DBA9F6E2D2C9796008ECB92;
+ remoteInfo = Waterfall_UI;
+ };
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -355,6 +372,9 @@
8D9A38582D194A41009A4186 /* WaterfallFile_UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WaterfallFile_UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8D9A38622D194A41009A4186 /* WaterfallFile_UIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WaterfallFile_UIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8D9A38772D196510009A4186 /* waterfall_file */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = waterfall_file; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8DBA9F6F2D2C9796008ECB92 /* Waterfall_UI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Waterfall_UI.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8DBA9F812D2C979A008ECB92 /* Waterfall_UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Waterfall_UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 8DBA9F8B2D2C979A008ECB92 /* Waterfall_UIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Waterfall_UIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
8DD98C412CC592540062D678 /* prysdr */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = prysdr; sourceTree = BUILT_PRODUCTS_DIR; };
8DD98C722CC632040062D678 /* MatrixXT.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MatrixXT.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -814,6 +834,7 @@
membershipExceptions = (
FileReader.swift,
PathUtils.swift,
+ Version.swift,
);
target = 8D9A38762D196510009A4186 /* WaterfallFile */;
};
@@ -833,6 +854,22 @@
);
target = 8D9A38452D194A3D009A4186 /* WaterfallFile_UI */;
};
+ 8DBA9F9E2D313168008ECB92 /* Exceptions for "Utils" folder in "Waterfall_UI" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Version.swift,
+ );
+ target = 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */;
+ };
+ 8DBA9FA72D37CD92008ECB92 /* Exceptions for "Radio" folder in "Waterfall_UI" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ HW/RtlSdr/r820/r820.swift,
+ Utils/WaterfallFile/NaiveFFT512.swift,
+ Utils/WaterfallFile/SimpleImage.swift,
+ );
+ target = 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */;
+ };
8DD98C7C2CC6320C0062D678 /* Exceptions for "LA" folder in "PrySDR" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
@@ -876,6 +913,21 @@
path = WaterfallFile_UIUITests;
sourceTree = "<group>";
};
+ 8DBA9F702D2C9796008ECB92 /* Waterfall_UI */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = Waterfall_UI;
+ sourceTree = "<group>";
+ };
+ 8DBA9F842D2C979A008ECB92 /* Waterfall_UITests */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = Waterfall_UITests;
+ sourceTree = "<group>";
+ };
+ 8DBA9F8E2D2C979A008ECB92 /* Waterfall_UIUITests */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = Waterfall_UIUITests;
+ sourceTree = "<group>";
+ };
8DD98C432CC592540062D678 /* PrySDR */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = PrySDR;
@@ -909,6 +961,7 @@
8D9A334E2D0B0D92009A4186 /* Exceptions for "Radio" folder in "BladeRFIQ" target */,
8DBA9F602D2928DE008ECB92 /* Exceptions for "Radio" folder in "WaterfallFile_UI" target */,
8D9A38812D196520009A4186 /* Exceptions for "Radio" folder in "WaterfallFile" target */,
+ 8DBA9FA72D37CD92008ECB92 /* Exceptions for "Radio" folder in "Waterfall_UI" target */,
);
path = Radio;
sourceTree = "<group>";
@@ -969,6 +1022,7 @@
8D9A37F12D180D4C009A4186 /* Exceptions for "Utils" folder in "BladeRFIQ" target */,
8DBA9F652D2B5158008ECB92 /* Exceptions for "Utils" folder in "WaterfallFile_UI" target */,
8D9A38832D19D277009A4186 /* Exceptions for "Utils" folder in "WaterfallFile" target */,
+ 8DBA9F9E2D313168008ECB92 /* Exceptions for "Utils" folder in "Waterfall_UI" target */,
);
path = Utils;
sourceTree = "<group>";
@@ -1121,6 +1175,30 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8DBA9F6C2D2C9796008ECB92 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8DBA9FA52D37C5D1008ECB92 /* ArgumentParser in Frameworks */,
+ 8DBA9FA32D37C5CD008ECB92 /* libusb.a in Frameworks */,
+ 8DBA9FA22D37C5C8008ECB92 /* libr820.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F7E2D2C979A008ECB92 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F882D2C979A008ECB92 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
8DD98C3E2CC592540062D678 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -1171,6 +1249,9 @@
8D9A38472D194A3D009A4186 /* WaterfallFile_UI */,
8D9A385B2D194A41009A4186 /* WaterfallFile_UITests */,
8D9A38652D194A41009A4186 /* WaterfallFile_UIUITests */,
+ 8DBA9F702D2C9796008ECB92 /* Waterfall_UI */,
+ 8DBA9F842D2C979A008ECB92 /* Waterfall_UITests */,
+ 8DBA9F8E2D2C979A008ECB92 /* Waterfall_UIUITests */,
8DD98C422CC592540062D678 /* Products */,
8DD98C642CC599CA0062D678 /* Series */,
8DD98C7F2CC7852E0062D678 /* Images */,
@@ -1200,6 +1281,9 @@
8D9A38582D194A41009A4186 /* WaterfallFile_UITests.xctest */,
8D9A38622D194A41009A4186 /* WaterfallFile_UIUITests.xctest */,
8D9A38772D196510009A4186 /* waterfall_file */,
+ 8DBA9F6F2D2C9796008ECB92 /* Waterfall_UI.app */,
+ 8DBA9F812D2C979A008ECB92 /* Waterfall_UITests.xctest */,
+ 8DBA9F8B2D2C979A008ECB92 /* Waterfall_UIUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@@ -1607,6 +1691,75 @@
productReference = 8D9A38772D196510009A4186 /* waterfall_file */;
productType = "com.apple.product-type.tool";
};
+ 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8DBA9F932D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UI" */;
+ buildPhases = (
+ 8DBA9F6B2D2C9796008ECB92 /* Sources */,
+ 8DBA9F6C2D2C9796008ECB92 /* Frameworks */,
+ 8DBA9F6D2D2C9796008ECB92 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 8DBA9F702D2C9796008ECB92 /* Waterfall_UI */,
+ );
+ name = Waterfall_UI;
+ packageProductDependencies = (
+ 8DBA9FA42D37C5D1008ECB92 /* ArgumentParser */,
+ );
+ productName = Waterfall_UI;
+ productReference = 8DBA9F6F2D2C9796008ECB92 /* Waterfall_UI.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 8DBA9F802D2C979A008ECB92 /* Waterfall_UITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8DBA9F962D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UITests" */;
+ buildPhases = (
+ 8DBA9F7D2D2C979A008ECB92 /* Sources */,
+ 8DBA9F7E2D2C979A008ECB92 /* Frameworks */,
+ 8DBA9F7F2D2C979A008ECB92 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 8DBA9F832D2C979A008ECB92 /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ 8DBA9F842D2C979A008ECB92 /* Waterfall_UITests */,
+ );
+ name = Waterfall_UITests;
+ packageProductDependencies = (
+ );
+ productName = Waterfall_UITests;
+ productReference = 8DBA9F812D2C979A008ECB92 /* Waterfall_UITests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 8DBA9F8A2D2C979A008ECB92 /* Waterfall_UIUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 8DBA9F992D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UIUITests" */;
+ buildPhases = (
+ 8DBA9F872D2C979A008ECB92 /* Sources */,
+ 8DBA9F882D2C979A008ECB92 /* Frameworks */,
+ 8DBA9F892D2C979A008ECB92 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 8DBA9F8D2D2C979A008ECB92 /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ 8DBA9F8E2D2C979A008ECB92 /* Waterfall_UIUITests */,
+ );
+ name = Waterfall_UIUITests;
+ packageProductDependencies = (
+ );
+ productName = Waterfall_UIUITests;
+ productReference = 8DBA9F8B2D2C979A008ECB92 /* Waterfall_UIUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
8DD98C402CC592540062D678 /* PrySDR */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8DD98C482CC592540062D678 /* Build configuration list for PBXNativeTarget "PrySDR" */;
@@ -1728,6 +1881,17 @@
8D9A38762D196510009A4186 = {
CreatedOnToolsVersion = 16.2;
};
+ 8DBA9F6E2D2C9796008ECB92 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ 8DBA9F802D2C979A008ECB92 = {
+ CreatedOnToolsVersion = 16.2;
+ TestTargetID = 8DBA9F6E2D2C9796008ECB92;
+ };
+ 8DBA9F8A2D2C979A008ECB92 = {
+ CreatedOnToolsVersion = 16.2;
+ TestTargetID = 8DBA9F6E2D2C9796008ECB92;
+ };
8DD98C402CC592540062D678 = {
CreatedOnToolsVersion = 16.0;
};
@@ -1773,6 +1937,9 @@
8D9A38572D194A41009A4186 /* WaterfallFile_UITests */,
8D9A38612D194A41009A4186 /* WaterfallFile_UIUITests */,
8D9A38762D196510009A4186 /* WaterfallFile */,
+ 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */,
+ 8DBA9F802D2C979A008ECB92 /* Waterfall_UITests */,
+ 8DBA9F8A2D2C979A008ECB92 /* Waterfall_UIUITests */,
);
};
/* End PBXProject section */
@@ -1799,6 +1966,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8DBA9F6D2D2C9796008ECB92 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F7F2D2C979A008ECB92 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F892D2C979A008ECB92 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
8DD98C702CC632040062D678 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1928,6 +2116,27 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 8DBA9F6B2D2C9796008ECB92 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F7D2D2C979A008ECB92 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8DBA9F872D2C979A008ECB92 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
8DD98C3D2CC592540062D678 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -2065,6 +2274,16 @@
target = 8D9A38762D196510009A4186 /* WaterfallFile */;
targetProxy = 8D9A38882D19D4E1009A4186 /* PBXContainerItemProxy */;
};
+ 8DBA9F832D2C979A008ECB92 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */;
+ targetProxy = 8DBA9F822D2C979A008ECB92 /* PBXContainerItemProxy */;
+ };
+ 8DBA9F8D2D2C979A008ECB92 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 8DBA9F6E2D2C9796008ECB92 /* Waterfall_UI */;
+ targetProxy = 8DBA9F8C2D2C979A008ECB92 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -2585,6 +2804,132 @@
};
name = Release;
};
+ 8DBA9F942D2C979A008ECB92 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = Waterfall_UI/Waterfall_UI.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Waterfall_UI/Preview Content\"";
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UI";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 8DBA9F952D2C979A008ECB92 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = Waterfall_UI/Waterfall_UI.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "\"Waterfall_UI/Preview Content\"";
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UI";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 8DBA9F972D2C979A008ECB92 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Waterfall_UI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Waterfall_UI";
+ };
+ name = Debug;
+ };
+ 8DBA9F982D2C979A008ECB92 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Waterfall_UI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Waterfall_UI";
+ };
+ name = Release;
+ };
+ 8DBA9F9A2D2C979A008ECB92 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UIUITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = Waterfall_UI;
+ };
+ name = Debug;
+ };
+ 8DBA9F9B2D2C979A008ECB92 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = 53B26AJZ4Z;
+ GENERATE_INFOPLIST_FILE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.2;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "RadioTeam.Waterfall-UIUITests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_VERSION = 5.0;
+ TEST_TARGET_NAME = Waterfall_UI;
+ };
+ name = Release;
+ };
8DD98C462CC592540062D678 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -2926,6 +3271,33 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 8DBA9F932D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UI" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8DBA9F942D2C979A008ECB92 /* Debug */,
+ 8DBA9F952D2C979A008ECB92 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 8DBA9F962D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8DBA9F972D2C979A008ECB92 /* Debug */,
+ 8DBA9F982D2C979A008ECB92 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 8DBA9F992D2C979A008ECB92 /* Build configuration list for PBXNativeTarget "Waterfall_UIUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 8DBA9F9A2D2C979A008ECB92 /* Debug */,
+ 8DBA9F9B2D2C979A008ECB92 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
8DD98C3C2CC592540062D678 /* Build configuration list for PBXProject "PrySDR" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -2997,6 +3369,11 @@
package = 8D0349232CF70E180026DA77 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
productName = ArgumentParser;
};
+ 8DBA9FA42D37C5D1008ECB92 /* ArgumentParser */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 8D0349232CF70E180026DA77 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
+ productName = ArgumentParser;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = 8DD98C392CC592540062D678 /* Project object */;
diff --git a/Radio/Utils/WaterfallFile/NaiveFFT512.swift b/Radio/Utils/WaterfallFile/NaiveFFT512.swift
index b4dca7e..0e6373a 100644
--- a/Radio/Utils/WaterfallFile/NaiveFFT512.swift
+++ b/Radio/Utils/WaterfallFile/NaiveFFT512.swift
@@ -46,4 +46,28 @@ class NaiveFFT512 {
return transform_result
}
+ func computeLine1024(_ processingArray: [Int8]) -> [Float] {
+
+ var dataFloat = [Float](repeating: 0.0, count: NaiveFFT512.sampleCount*2)
+
+ if processingArray.count != NaiveFFT512.sampleCount*2 {
+ print("Not supporting arrays not equail to \(NaiveFFT512.sampleCount)")
+ return []
+ }
+
+ vDSP.convertElements(of: processingArray, to: &dataFloat)
+ //dataFloat = vDSP.add(127.0, dataFloat)
+ //print(dataFloat)
+
+ //move from -127.0 to 128.0 range -1.0...1.0
+ //var adjusted = vDSP.divide(dataFloat, Float(sampleCount))
+ var adjusted = dataFloat
+ //print(adjusted)
+
+ var transform_result = forwardDCT.transform(adjusted)
+ transform_result = vDSP.absolute(transform_result)
+
+ return transform_result
+ }
+
}
diff --git a/WaterfallFile_UI/ContentView.swift b/WaterfallFile_UI/ContentView.swift
index 92445d8..d6809b3 100644
--- a/WaterfallFile_UI/ContentView.swift
+++ b/WaterfallFile_UI/ContentView.swift
@@ -51,6 +51,7 @@ struct ContentView: View {
scale: 1,
orientation: .left)
//.resizable()
+ .rotationEffect(Angle(degrees: 90))
//Image(fileSpectrum.makeSpectrogramImage(), scale: 1, orientation:.left, label: "Image")
diff --git a/WaterfallFile_UI/FileSpectrum.swift b/WaterfallFile_UI/FileSpectrum.swift
index ce64836..f816f37 100644
--- a/WaterfallFile_UI/FileSpectrum.swift
+++ b/WaterfallFile_UI/FileSpectrum.swift
@@ -96,7 +96,7 @@ class FileSpectrum: NSObject, ObservableObject {
sessionQueue.async {
print("lets start the task for spectrum analysis")
for i in 0..<512 {
- print("Process the task \(i)")
+ print("Process the task line \(i)")
self.simpleImage.drawPixel(i, i, PixelData(a: 255, r: 255, g: 0, b: 0))
DispatchQueue.main.async {
self.dataFileProcessor()
diff --git a/Waterfall_UI/Assets.xcassets/AccentColor.colorset/Contents.json b/Waterfall_UI/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Waterfall_UI/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Waterfall_UI/Assets.xcassets/AppIcon.appiconset/Contents.json b/Waterfall_UI/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..3f00db4
--- /dev/null
+++ b/Waterfall_UI/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,58 @@
+{
+ "images" : [
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Waterfall_UI/Assets.xcassets/Contents.json b/Waterfall_UI/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Waterfall_UI/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Waterfall_UI/ContentView.swift b/Waterfall_UI/ContentView.swift
new file mode 100644
index 0000000..6c09fec
--- /dev/null
+++ b/Waterfall_UI/ContentView.swift
@@ -0,0 +1,194 @@
+//
+// ContentView.swift
+// Waterfall_UI
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import SwiftUI
+import SwiftData
+
+private struct DeviceChoose: Identifiable {
+ let deviceName: String
+ var id: String { deviceName }
+}
+
+private let deviceList: [DeviceChoose] = [
+ DeviceChoose(deviceName:"RtlSDR SN:00000000"),
+ DeviceChoose(deviceName:"BladeRF SN:00000000"),
+ DeviceChoose(deviceName:"AirSpy SN:00000000"),
+ DeviceChoose(deviceName:"AirSpyHF SN:00000000")
+]
+
+private struct SampleRateChoose: Identifiable {
+ let sampleRate: Int
+ var id: String { String(sampleRate) }
+}
+
+private let sampleRateList: [SampleRateChoose] = [
+ SampleRateChoose(sampleRate:1000),
+ SampleRateChoose(sampleRate:100000)
+]
+
+private struct FFTOptions: Identifiable {
+ let binsize: Int
+ var id: String { String(binsize) }
+}
+
+private let fftBinsList: [FFTOptions] = [
+ FFTOptions(binsize: 512),
+ FFTOptions(binsize: 1024),
+ FFTOptions(binsize: 2048),
+ FFTOptions(binsize: 4096),
+]
+
+struct ContentView: View {
+ @Environment(\.modelContext) private var modelContext
+
+ @EnvironmentObject var sdrSpectrum: SDRSpectrum
+
+ @Query private var items: [Item]
+
+ @State private var selectedDevice: String = deviceList[0].deviceName
+ @State private var selectedSampleRate: String = String(sampleRateList[0].sampleRate)
+ @State private var setFrequency: Int = 100000000
+ @State private var toggleAGC: Bool = false
+ @State private var selectorFFTBin: String = String(fftBinsList[0].binsize)
+ @State private var statusMessage: String = ""
+
+
+ var body: some View {
+ /*
+ NavigationSplitView {
+ List {
+ ForEach(items) { item in
+ NavigationLink {
+ Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
+ } label: {
+ Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
+ }
+ }
+ .onDelete(perform: deleteItems)
+ }
+ .navigationSplitViewColumnWidth(min: 180, ideal: 200)
+ .toolbar {
+ ToolbarItem {
+ Button(action: addItem) {
+ Label("Add Item", systemImage: "plus")
+ }
+ }
+ }
+ } detail: {
+ Text("Select an item")
+ }
+ */
+ HStack {
+ VStack {
+ Text("Hardware")//.border(.red)
+ Picker("Device", selection:$selectedDevice) {
+ ForEach(deviceList) { device in
+ Text("\(device.deviceName)")
+ }
+ }
+ .onChange(of: selectedDevice) {
+ statusMessage = "selected \(selectedDevice)"
+ }
+ Picker("SampleRate", selection:$selectedSampleRate) {
+ ForEach(sampleRateList) { samplerate in
+ Text("\(samplerate.sampleRate)")
+ }
+ }.onChange(of:selectedSampleRate) {
+ statusMessage = "selected \(selectedSampleRate)"
+ }
+ Divider()
+ HStack {
+ Text("Control")
+ Button("Start") {
+ print("Start")
+ statusMessage = "Running"
+ }
+ Button("Stop") {
+ print("Stop")
+ statusMessage = "Idle"
+ }
+ }
+ Divider()
+ Text("Config")
+ HStack {
+ Text("Frequency: ")
+ TextField("Frquency: ", value: $setFrequency, format: .number)
+ .onChange(of: setFrequency) {
+ statusMessage = "frequency \(setFrequency)"
+ }
+ .onSubmit {
+ statusMessage = "frequency submit"
+ }
+ Text("Hz")
+ }
+ Toggle("AGC",isOn: $toggleAGC)
+ .onChange(of: toggleAGC) {
+ if (self.toggleAGC) {
+ statusMessage = "AGC on"
+ } else {
+ statusMessage = "AGC off"
+ }
+ }
+ Divider()
+ Text("FFT View")
+ Picker("FFT bins", selection:$selectorFFTBin) {
+ ForEach(fftBinsList) { binssize in
+ Text("\(binssize.binsize)")
+ }
+ }
+ .onChange(of: selectorFFTBin) {
+ statusMessage = "set fft bins \(selectorFFTBin)"
+ }
+ Spacer()
+ Divider()//.border(.red)
+
+ HStack {
+ Text("Version: \(software_version)")//.border(.red)
+ Spacer()
+ }
+ }
+ Divider()
+ VStack {
+ //Text("Right")//.border(.red)
+ Divider()
+ Image(decorative: sdrSpectrum.outputImage,
+ scale: 1,
+ orientation: .left)
+ .resizable()
+ .scaledToFit()
+ .rotationEffect(Angle(degrees: 90))
+ Spacer()
+ Divider()//.border(.red)
+
+ HStack {
+ Spacer()
+ Text("Status message: \(statusMessage)")//.border(.red)
+ }
+ }
+ }
+ }
+
+ private func addItem() {
+ withAnimation {
+ let newItem = Item(timestamp: Date())
+ modelContext.insert(newItem)
+ }
+ }
+
+ private func deleteItems(offsets: IndexSet) {
+ withAnimation {
+ for index in offsets {
+ modelContext.delete(items[index])
+ }
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+ .modelContainer(for: Item.self, inMemory: true)
+}
diff --git a/Waterfall_UI/Item.swift b/Waterfall_UI/Item.swift
new file mode 100644
index 0000000..0f2a8ae
--- /dev/null
+++ b/Waterfall_UI/Item.swift
@@ -0,0 +1,18 @@
+//
+// Item.swift
+// Waterfall_UI
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import Foundation
+import SwiftData
+
+@Model
+final class Item {
+ var timestamp: Date
+
+ init(timestamp: Date) {
+ self.timestamp = timestamp
+ }
+}
diff --git a/Waterfall_UI/Preview Content/Preview Assets.xcassets/Contents.json b/Waterfall_UI/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Waterfall_UI/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Waterfall_UI/SDRSpectrum.swift b/Waterfall_UI/SDRSpectrum.swift
new file mode 100644
index 0000000..c83e802
--- /dev/null
+++ b/Waterfall_UI/SDRSpectrum.swift
@@ -0,0 +1,137 @@
+//
+// SDRSpectrum.swift
+// PrySDR
+//
+// Created by Jacky Jack on 10/01/2025.
+//
+
+//
+// FileSpectrum.swift
+// PrySDR
+//
+// Created by Jacky Jack on 01/01/2025.
+//
+
+import Accelerate
+import AppKit
+import Combine
+import CoreImage
+
+import libr820
+
+class SDRSpectrum: NSObject, ObservableObject {
+
+ static let default_width = 512
+ static let default_height = 512
+ static let defaultSleepTime:UInt32 = 1
+ var offset:Int=0
+ var allData:[Int8]? = nil
+ var line_counter:Int = 0
+
+ //SDR default configs
+ let device_idx: UInt32 = 0
+ let samplerate: Int = 2048000
+ let gain: Int = 0
+ let frequency: Int = 99500000
+ let nsamples: Int = default_width
+ var device: R820Tuner?
+
+ //fft drawing related
+ let fft512 = NaiveFFT512()
+
+ let sessionQueue = DispatchQueue(label: "sessionQueue",
+ attributes: [],
+ autoreleaseFrequency: .workItem)
+
+ @Published var outputImage = emptyCGImage
+
+ let simpleImage = SimpleImage(width: default_width, height: default_height)
+
+ /// A 1x1 Core Graphics image.
+ static var emptyCGImage: CGImage = {
+ let buffer = vImage.PixelBuffer(
+ pixelValues: [0],
+ size: .init(width: 1, height: 1),
+ pixelFormat: vImage.Planar8.self)
+
+ let fmt = vImage_CGImageFormat(
+ bitsPerComponent: 8,
+ bitsPerPixel: 8 ,
+ colorSpace: CGColorSpaceCreateDeviceGray(),
+ bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
+ renderingIntent: .defaultIntent)
+
+ return buffer.makeCGImage(cgImageFormat: fmt!)!
+ }()
+
+ func makeSpectrogramImage() -> NSImage {
+
+ return NSImage(cgImage: SDRSpectrum.emptyCGImage, size: .zero)
+ }
+
+ //just get data time to time from databuffer and pretend to be realtime data
+ func dataFileProcessor() {
+
+
+ }
+
+ func startRunning() {
+ let buf_ptr = UnsafeMutableRawPointer.allocate(byteCount: nsamples, alignment: 1)
+ var nbytes:Int32 = 0
+ //var total_bytes:Int32 = 0
+
+
+ //prepare sdr
+ print("Prepare SDR")
+ let count = getDeviceCount()
+ print("Found \(count) devices")
+ self.device = R820Tuner()
+ sleep(1)
+ if (self.device != nil) {
+ self.device!.open(index:0)
+
+ //prepare dongle to receive some data
+ let _ = self.device!.setSampleRate(samplerate: UInt32(samplerate))
+ let _ = self.device!.setCenterFreq(freq: UInt32(frequency))
+ let _ = self.device!.setAgcMode(on: 1)
+ let _ = self.device!.resetBuffer()
+ }
+ //run the task
+ sessionQueue.async {
+ print("lets start the task for spectrum analysis")
+ for i in 0..<512 {
+
+ if let dev = self.device {
+ let ret = dev.readSync(buf: buf_ptr, len: Int32(self.nsamples), n_read: &nbytes)
+ if ret<0 {
+ print("data read sync returned <0 = \(ret)")
+ sleep(1);
+ continue;
+ }
+ }
+ print("Got \(nbytes) bytes")
+
+ /*
+ let dataU8 = buf_ptr.bindMemory(to: UInt8.self, capacity: self.nsamples)
+ let bufferU8 = UnsafeBufferPointer(start: dataU8, count: self.nsamples)
+ let convertedData = Data(bufferU8)
+ */
+ let dataI8 = buf_ptr.bindMemory(to: Int8.self, capacity: self.nsamples)
+ let bufferI8 = UnsafeBufferPointer(start: dataI8, count: self.nsamples)
+ let safeI8 = Array(bufferI8)
+
+ let transform_result = self.fft512.computeLine(safeI8)
+ print(transform_result)
+ print("FFT result \(transform_result.count) points")
+ //let drawSlice = transform_result[0...SDRSpectrum.default_width-1]
+ //print("FFT result \(drawSlice.count) points")
+ self.simpleImage.drawPalletLine(line: i, pixelLine: transform_result)
+ self.outputImage = self.simpleImage.toCGImage()!
+
+ print("Process the task line \(i)")
+ sleep(1)
+ }
+ print("SDR loop done run")
+ }
+ }
+}
diff --git a/Waterfall_UI/Waterfall_UI.entitlements b/Waterfall_UI/Waterfall_UI.entitlements
new file mode 100644
index 0000000..9c8ecca
--- /dev/null
+++ b/Waterfall_UI/Waterfall_UI.entitlements
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+ <key>com.apple.security.device.usb</key>
+ <true/>
+ <key>com.apple.security.files.user-selected.read-only</key>
+ <true/>
+</dict>
+</plist>
diff --git a/Waterfall_UI/Waterfall_UIApp.swift b/Waterfall_UI/Waterfall_UIApp.swift
new file mode 100644
index 0000000..6d9a13b
--- /dev/null
+++ b/Waterfall_UI/Waterfall_UIApp.swift
@@ -0,0 +1,46 @@
+//
+// Waterfall_UIApp.swift
+// Waterfall_UI
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import SwiftUI
+import SwiftData
+
+
+@main
+struct Waterfall_UIApp: App {
+
+ let sdrSpectrum = SDRSpectrum()
+
+ @Environment(\.scenePhase) private var scenePhase
+
+ var sharedModelContainer: ModelContainer = {
+ let schema = Schema([
+ Item.self,
+ ])
+ let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
+
+ do {
+ return try ModelContainer(for: schema, configurations: [modelConfiguration])
+ } catch {
+ fatalError("Could not create ModelContainer: \(error)")
+ }
+ }()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environmentObject(sdrSpectrum)
+ .onChange(of: scenePhase) { phase in
+ if phase == .active {
+ Task(priority: .userInitiated) {
+ sdrSpectrum.startRunning()
+ }
+ }
+ }
+ }
+ .modelContainer(sharedModelContainer)
+ }
+}
diff --git a/Waterfall_UITests/Waterfall_UITests.swift b/Waterfall_UITests/Waterfall_UITests.swift
new file mode 100644
index 0000000..6f9a614
--- /dev/null
+++ b/Waterfall_UITests/Waterfall_UITests.swift
@@ -0,0 +1,17 @@
+//
+// Waterfall_UITests.swift
+// Waterfall_UITests
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import Testing
+@testable import Waterfall_UI
+
+struct Waterfall_UITests {
+
+ @Test func example() async throws {
+ // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ }
+
+}
diff --git a/Waterfall_UIUITests/Waterfall_UIUITests.swift b/Waterfall_UIUITests/Waterfall_UIUITests.swift
new file mode 100644
index 0000000..4b8348c
--- /dev/null
+++ b/Waterfall_UIUITests/Waterfall_UIUITests.swift
@@ -0,0 +1,43 @@
+//
+// Waterfall_UIUITests.swift
+// Waterfall_UIUITests
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import XCTest
+
+final class Waterfall_UIUITests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+
+ // In UI tests it is usually best to stop immediately when a failure occurs.
+ continueAfterFailure = false
+
+ // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ @MainActor
+ func testExample() throws {
+ // UI tests must launch the application that they test.
+ let app = XCUIApplication()
+ app.launch()
+
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+ }
+
+ @MainActor
+ func testLaunchPerformance() throws {
+ if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
+ // This measures how long it takes to launch your application.
+ measure(metrics: [XCTApplicationLaunchMetric()]) {
+ XCUIApplication().launch()
+ }
+ }
+ }
+}
diff --git a/Waterfall_UIUITests/Waterfall_UIUITestsLaunchTests.swift b/Waterfall_UIUITests/Waterfall_UIUITestsLaunchTests.swift
new file mode 100644
index 0000000..d4d2fb8
--- /dev/null
+++ b/Waterfall_UIUITests/Waterfall_UIUITestsLaunchTests.swift
@@ -0,0 +1,33 @@
+//
+// Waterfall_UIUITestsLaunchTests.swift
+// Waterfall_UIUITests
+//
+// Created by Jacky Jack on 06/01/2025.
+//
+
+import XCTest
+
+final class Waterfall_UIUITestsLaunchTests: XCTestCase {
+
+ override class var runsForEachTargetApplicationUIConfiguration: Bool {
+ true
+ }
+
+ override func setUpWithError() throws {
+ continueAfterFailure = false
+ }
+
+ @MainActor
+ func testLaunch() throws {
+ let app = XCUIApplication()
+ app.launch()
+
+ // Insert steps here to perform after app launch but before taking a screenshot,
+ // such as logging into a test account or navigating somewhere in the app
+
+ let attachment = XCTAttachment(screenshot: app.screenshot())
+ attachment.name = "Launch Screen"
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ }
+}