Fix DX-Smart provisioning protocol and add debug logging
Fix critical packet format bugs matching SDK: frame select/type/trigger/disable commands now send empty data, RSSI@1m corrected to -59 dBm. Add DebugLog, read-config mode, service point list, and dev scheme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8c2320da44
commit
5283d2d265
10 changed files with 1735 additions and 264 deletions
|
|
@ -11,13 +11,21 @@
|
|||
7D757E1341A0143A2E9EBDF4 /* Pods_PayfritBeacon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6819A0F2BD2E9D84E7EDB4 /* Pods_PayfritBeacon.framework */; };
|
||||
D01000000001 /* PayfritBeaconApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000001 /* PayfritBeaconApp.swift */; };
|
||||
D01000000002 /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000002 /* Api.swift */; };
|
||||
D01000000003 /* BeaconBanList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000003 /* BeaconBanList.swift */; };
|
||||
D01000000004 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000004 /* BeaconScanner.swift */; };
|
||||
D01000000005 /* DevBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000005 /* DevBanner.swift */; };
|
||||
D01000000006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000006 /* LoginView.swift */; };
|
||||
D01000000007 /* BusinessListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000007 /* BusinessListView.swift */; };
|
||||
D01000000008 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000008 /* ScanView.swift */; };
|
||||
D01000000009 /* QrScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02000000009 /* QrScanView.swift */; };
|
||||
D0100000000A /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0200000000A /* RootView.swift */; };
|
||||
D01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D02000000060 /* Assets.xcassets */; };
|
||||
D01000000070 /* payfrit-favicon-light-outlines.svg in Resources */ = {isa = PBXBuildFile; fileRef = D02000000070 /* payfrit-favicon-light-outlines.svg */; };
|
||||
D01000000080 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D02000000080 /* InfoPlist.strings */; };
|
||||
D010000000B1 /* BLEBeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8710F334FDFE10F0625EB86D /* BLEBeaconScanner.swift */; };
|
||||
D010000000B2 /* BeaconProvisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C275738594E225BE7D5740 /* BeaconProvisioner.swift */; };
|
||||
D010000000B3 /* BeaconShardPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 964B63D1857877BBEE73F1D1 /* BeaconShardPool.swift */; };
|
||||
F1575ED0F871FE8806035906 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
|
@ -42,6 +50,7 @@
|
|||
D02000000070 /* payfrit-favicon-light-outlines.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = "payfrit-favicon-light-outlines.svg"; sourceTree = "<group>"; };
|
||||
D02000000081 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
E1775119CBC98A753AE26D84 /* ServicePointListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ServicePointListView.swift; sourceTree = "<group>"; };
|
||||
F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DebugLog.swift; path = PayfritBeacon/DebugLog.swift; sourceTree = "<group>"; };
|
||||
F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PayfritBeacon.release.xcconfig"; path = "Target Support Files/Pods-PayfritBeacon/Pods-PayfritBeacon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -73,6 +82,7 @@
|
|||
C05000000009 /* Products */,
|
||||
04996117E2F5D5BB2D86CD46 /* Pods */,
|
||||
EEC06FED6BE78CF9357F3158 /* Frameworks */,
|
||||
F100AA6D1E41596FCB1C1A39 /* DebugLog.swift */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -234,11 +244,19 @@
|
|||
files = (
|
||||
D01000000001 /* PayfritBeaconApp.swift in Sources */,
|
||||
D01000000002 /* Api.swift in Sources */,
|
||||
D01000000003 /* BeaconBanList.swift in Sources */,
|
||||
D01000000004 /* BeaconScanner.swift in Sources */,
|
||||
D01000000005 /* DevBanner.swift in Sources */,
|
||||
D01000000006 /* LoginView.swift in Sources */,
|
||||
D01000000007 /* BusinessListView.swift in Sources */,
|
||||
D01000000008 /* ScanView.swift in Sources */,
|
||||
D01000000009 /* QrScanView.swift in Sources */,
|
||||
D0100000000A /* RootView.swift in Sources */,
|
||||
D010000000B1 /* BLEBeaconScanner.swift in Sources */,
|
||||
D010000000B2 /* BeaconProvisioner.swift in Sources */,
|
||||
D010000000B3 /* BeaconShardPool.swift in Sources */,
|
||||
281CC856DD918C4CFA00EB67 /* ServicePointListView.swift in Sources */,
|
||||
F1575ED0F871FE8806035906 /* DebugLog.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -256,6 +274,193 @@
|
|||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
064DDD2A9238EC6900250593 /* Release-Dev */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_DISPLAY_NAME = "Payfrit Beacon BETA";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = PayfritBeacon/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Payfrit Beacon";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.beacon.dev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEV;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = "Release-Dev";
|
||||
};
|
||||
672BF8AE9DE36DEAE34D6DA0 /* Debug-Dev */ = {
|
||||
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++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = "Debug-Dev";
|
||||
};
|
||||
99070AFE15F557412BACA2C9 /* Release-Dev */ = {
|
||||
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++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = "Release-Dev";
|
||||
};
|
||||
B0D496FEA252D8DDA33F57A0 /* Debug-Dev */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_DISPLAY_NAME = "Payfrit Beacon BETA";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = PayfritBeacon/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Payfrit Beacon";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.business";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.beacon.dev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG DEV";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = "Debug-Dev";
|
||||
};
|
||||
C0B000000001 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
|
@ -377,6 +582,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AFF542870BA2862FAB429A86 /* Pods-PayfritBeacon.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_DISPLAY_NAME = "Payfrit Beacon";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -400,6 +606,7 @@
|
|||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -410,6 +617,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F1B84CA677516C50F6BE2294 /* Pods-PayfritBeacon.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_DISPLAY_NAME = "Payfrit Beacon";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -433,6 +641,7 @@
|
|||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -447,6 +656,8 @@
|
|||
buildConfigurations = (
|
||||
C0B000000001 /* Debug */,
|
||||
C0B000000002 /* Release */,
|
||||
672BF8AE9DE36DEAE34D6DA0 /* Debug-Dev */,
|
||||
99070AFE15F557412BACA2C9 /* Release-Dev */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
|
|
@ -456,6 +667,8 @@
|
|||
buildConfigurations = (
|
||||
C0B000000003 /* Debug */,
|
||||
C0B000000004 /* Release */,
|
||||
B0D496FEA252D8DDA33F57A0 /* Debug-Dev */,
|
||||
064DDD2A9238EC6900250593 /* Release-Dev */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C06000000001"
|
||||
BuildableName = "PayfritBeacon.app"
|
||||
BlueprintName = "PayfritBeacon"
|
||||
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug-Dev"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug-Dev"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C06000000001"
|
||||
BuildableName = "PayfritBeacon.app"
|
||||
BlueprintName = "PayfritBeacon"
|
||||
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release-Dev"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C06000000001"
|
||||
BuildableName = "PayfritBeacon.app"
|
||||
BlueprintName = "PayfritBeacon"
|
||||
ReferencedContainer = "container:PayfritBeacon.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug-Dev">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release-Dev"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -3,8 +3,12 @@ import Foundation
|
|||
class Api {
|
||||
static let shared = Api()
|
||||
|
||||
// ── DEV toggle: flip to false for production ──
|
||||
// ── DEV toggle: driven by DEV compiler flag (set in build configuration) ──
|
||||
#if DEV
|
||||
static let IS_DEV = true
|
||||
#else
|
||||
static let IS_DEV = false
|
||||
#endif
|
||||
|
||||
private static var BASE_URL: String {
|
||||
IS_DEV ? "https://dev.payfrit.com/api" : "https://biz.payfrit.com/api"
|
||||
|
|
@ -280,51 +284,69 @@ class Api {
|
|||
|
||||
/// Get beacon config for a service point (UUID, Major, Minor to write to beacon)
|
||||
func getBeaconConfig(businessId: Int, servicePointId: Int) async throws -> BeaconConfigResponse {
|
||||
DebugLog.shared.log("[API] getBeaconConfig businessId=\(businessId) servicePointId=\(servicePointId)")
|
||||
|
||||
let json = try await postRequest(
|
||||
endpoint: "/beacon-sharding/get_beacon_config.cfm",
|
||||
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||
)
|
||||
|
||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to get beacon config"
|
||||
DebugLog.shared.log("[API] getBeaconConfig response keys: \(json.keys.sorted())")
|
||||
DebugLog.shared.log("[API] getBeaconConfig full response: \(json)")
|
||||
|
||||
if !parseBool(json["OK"] ?? json["ok"] ?? json["Ok"]) {
|
||||
let error = ((json["ERROR"] ?? json["error"] ?? json["Error"]) as? String) ?? "Failed to get beacon config"
|
||||
throw ApiException(error)
|
||||
}
|
||||
|
||||
guard let uuid = (json["UUID"] ?? json["uuid"]) as? String,
|
||||
let major = parseIntValue(json["MAJOR"] ?? json["major"]),
|
||||
let minor = parseIntValue(json["MINOR"] ?? json["minor"]) else {
|
||||
throw ApiException("Invalid beacon config response")
|
||||
guard let uuid = (json["UUID"] ?? json["uuid"] ?? json["Uuid"] ?? json["BeaconUUID"] ?? json["BEACONUUID"]) as? String else {
|
||||
throw ApiException("Invalid beacon config response - no UUID. Keys: \(json.keys.sorted())")
|
||||
}
|
||||
|
||||
guard let major = parseIntValue(json["MAJOR"] ?? json["major"] ?? json["Major"] ?? json["BeaconMajor"] ?? json["BEACONMAJOR"]) else {
|
||||
throw ApiException("Invalid beacon config response - no Major. Keys: \(json.keys.sorted())")
|
||||
}
|
||||
|
||||
guard let minor = parseIntValue(json["MINOR"] ?? json["minor"] ?? json["Minor"] ?? json["BeaconMinor"] ?? json["BEACONMINOR"]) else {
|
||||
throw ApiException("Invalid beacon config response - no Minor. Keys: \(json.keys.sorted())")
|
||||
}
|
||||
|
||||
DebugLog.shared.log("[API] getBeaconConfig parsed: uuid=\(uuid) major=\(major) minor=\(minor)")
|
||||
|
||||
return BeaconConfigResponse(
|
||||
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
||||
major: UInt16(major),
|
||||
minor: UInt16(minor),
|
||||
txPower: parseIntValue(json["TXPOWER"] ?? json["txPower"]) ?? -59,
|
||||
interval: parseIntValue(json["INTERVAL"] ?? json["interval"]) ?? 350
|
||||
txPower: parseIntValue(json["TXPOWER"] ?? json["txPower"] ?? json["TxPower"]) ?? -59,
|
||||
interval: parseIntValue(json["INTERVAL"] ?? json["interval"] ?? json["Interval"]) ?? 350
|
||||
)
|
||||
}
|
||||
|
||||
/// Register beacon hardware after provisioning
|
||||
func registerBeaconHardware(businessId: Int, servicePointId: Int, uuid: String, major: UInt16, minor: UInt16, macAddress: String?) async throws -> Bool {
|
||||
func registerBeaconHardware(businessId: Int, servicePointId: Int, uuid: String, major: UInt16, minor: UInt16, hardwareId: String, macAddress: String? = nil) async throws -> Bool {
|
||||
var body: [String: Any] = [
|
||||
"BusinessID": businessId,
|
||||
"ServicePointID": servicePointId,
|
||||
"UUID": uuid,
|
||||
"Major": major,
|
||||
"Minor": minor
|
||||
"Minor": minor,
|
||||
"HardwareID": hardwareId
|
||||
]
|
||||
if let mac = macAddress, !mac.isEmpty {
|
||||
body["MACAddress"] = mac
|
||||
}
|
||||
|
||||
DebugLog.shared.log("[API] registerBeaconHardware body: \(body)")
|
||||
|
||||
let json = try await postRequest(
|
||||
endpoint: "/beacon-sharding/register_beacon_hardware.cfm",
|
||||
body: body,
|
||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||
)
|
||||
|
||||
DebugLog.shared.log("[API] registerBeaconHardware response: \(json)")
|
||||
|
||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to register beacon"
|
||||
throw ApiException(error)
|
||||
|
|
@ -376,7 +398,8 @@ class Api {
|
|||
|
||||
return BusinessNamespace(
|
||||
shardId: parseIntValue(json["ShardID"] ?? json["SHARDID"]) ?? 0,
|
||||
uuid: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
||||
uuid: uuid,
|
||||
uuidClean: uuid.replacingOccurrences(of: "-", with: "").uppercased(),
|
||||
major: UInt16(major),
|
||||
alreadyAllocated: parseBool(json["AlreadyAllocated"] ?? json["ALREADYALLOCATED"])
|
||||
)
|
||||
|
|
@ -412,12 +435,16 @@ class Api {
|
|||
body["ServicePointID"] = spId
|
||||
}
|
||||
|
||||
DebugLog.shared.log("[API] saveServicePoint businessId=\(businessId) name=\(name) servicePointId=\(String(describing: servicePointId))")
|
||||
|
||||
let json = try await postRequest(
|
||||
endpoint: "/servicepoints/save.cfm",
|
||||
body: body,
|
||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||
)
|
||||
|
||||
DebugLog.shared.log("[API] saveServicePoint response: \(json)")
|
||||
|
||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to save service point"
|
||||
throw ApiException(error)
|
||||
|
|
@ -429,6 +456,12 @@ class Api {
|
|||
let spId = parseIntValue(sp["ServicePointID"] ?? sp["SERVICEPOINTID"]) ?? servicePointId ?? 0
|
||||
let beaconMinor = parseIntValue(sp["BeaconMinor"] ?? sp["BEACONMINOR"])
|
||||
|
||||
DebugLog.shared.log("[API] saveServicePoint parsed: spId=\(spId) beaconMinor=\(String(describing: beaconMinor))")
|
||||
|
||||
if spId == 0 {
|
||||
DebugLog.shared.log("[API] WARNING: servicePointId is 0! Full sp dict: \(sp)")
|
||||
}
|
||||
|
||||
return ServicePoint(
|
||||
servicePointId: spId,
|
||||
name: name,
|
||||
|
|
@ -442,6 +475,20 @@ class Api {
|
|||
return try await saveServicePoint(businessId: businessId, name: name)
|
||||
}
|
||||
|
||||
/// Delete a service point
|
||||
func deleteServicePoint(businessId: Int, servicePointId: Int) async throws {
|
||||
let json = try await postRequest(
|
||||
endpoint: "/servicepoints/delete.cfm",
|
||||
body: ["BusinessID": businessId, "ServicePointID": servicePointId],
|
||||
extraHeaders: ["X-Business-Id": String(businessId)]
|
||||
)
|
||||
|
||||
if !parseBool(json["OK"] ?? json["ok"]) {
|
||||
let error = ((json["ERROR"] ?? json["error"]) as? String) ?? "Failed to delete service point"
|
||||
throw ApiException(error)
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HELPERS
|
||||
// =========================================================================
|
||||
|
|
@ -550,7 +597,8 @@ struct ServicePoint: Identifiable {
|
|||
|
||||
struct BusinessNamespace {
|
||||
let shardId: Int
|
||||
let uuid: String // 32-char hex, no dashes
|
||||
let uuid: String // Original UUID from API (with dashes, as-is)
|
||||
let uuidClean: String // 32-char hex, no dashes, uppercase (for BLE provisioning)
|
||||
let major: UInt16
|
||||
let alreadyAllocated: Bool
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/// Beacon type detected by service UUID
|
||||
/// Beacon type detected by service UUID or name
|
||||
enum BeaconType: String {
|
||||
case kbeacon = "KBeacon"
|
||||
case dxsmart = "DX-Smart"
|
||||
case bluecharm = "BlueCharm"
|
||||
case unknown = "Unknown"
|
||||
}
|
||||
|
|
@ -18,8 +19,8 @@ struct DiscoveredBeacon: Identifiable {
|
|||
var lastSeen: Date
|
||||
|
||||
var displayName: String {
|
||||
if name.isEmpty || name == "Unknown" {
|
||||
return "\(type.rawValue) (\(id.uuidString.prefix(8))...)"
|
||||
if name.isEmpty {
|
||||
return id.uuidString.prefix(8) + "..."
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
|
@ -64,9 +65,9 @@ class BLEBeaconScanner: NSObject, ObservableObject {
|
|||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
||||
)
|
||||
|
||||
// Auto-stop after 10 seconds
|
||||
// Auto-stop after 1 second (beacons advertise every ~200ms so 1s is plenty)
|
||||
scanTimer?.invalidate()
|
||||
scanTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [weak self] _ in
|
||||
scanTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
|
||||
self?.stopScanning()
|
||||
}
|
||||
}
|
||||
|
|
@ -104,35 +105,20 @@ extension BLEBeaconScanner: CBCentralManagerDelegate {
|
|||
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
|
||||
let rssiValue = RSSI.intValue
|
||||
guard rssiValue > -90 && rssiValue < 0 else { return } // Filter weak signals
|
||||
guard rssiValue > -70 && rssiValue < 0 else { return } // Only show nearby devices
|
||||
|
||||
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
||||
|
||||
// Determine beacon type from name or advertised services
|
||||
// Best-effort type hint from advertised services (informational only)
|
||||
var beaconType: BeaconType = .unknown
|
||||
|
||||
// Check advertised service UUIDs
|
||||
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
||||
if serviceUUIDs.contains(BLEBeaconScanner.KBEACON_SERVICE) {
|
||||
beaconType = .kbeacon
|
||||
beaconType = .dxsmart
|
||||
} else if serviceUUIDs.contains(BLEBeaconScanner.BLUECHARM_SERVICE) {
|
||||
beaconType = .bluecharm
|
||||
}
|
||||
}
|
||||
|
||||
// Also check by name patterns
|
||||
if beaconType == .unknown {
|
||||
let lowerName = name.lowercased()
|
||||
if lowerName.contains("kbeacon") || lowerName.contains("kbpro") || lowerName.hasPrefix("kb") {
|
||||
beaconType = .kbeacon
|
||||
} else if lowerName.contains("bluecharm") || lowerName.contains("bc") || lowerName.hasPrefix("bc") {
|
||||
beaconType = .bluecharm
|
||||
}
|
||||
}
|
||||
|
||||
// Only track beacons we can identify
|
||||
guard beaconType != .unknown else { return }
|
||||
|
||||
// Update or add beacon
|
||||
if let index = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
||||
discoveredBeacons[index].rssi = rssiValue
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
33
PayfritBeacon/DebugLog.swift
Normal file
33
PayfritBeacon/DebugLog.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import Foundation
|
||||
|
||||
/// Simple in-app debug log — viewable from ScanView
|
||||
class DebugLog: ObservableObject {
|
||||
static let shared = DebugLog()
|
||||
|
||||
@Published var entries: [String] = []
|
||||
|
||||
private let maxEntries = 200
|
||||
|
||||
func log(_ message: String) {
|
||||
let ts = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
|
||||
let entry = "[\(ts)] \(message)"
|
||||
NSLog("[DebugLog] \(message)")
|
||||
DispatchQueue.main.async {
|
||||
self.entries.append(entry)
|
||||
if self.entries.count > self.maxEntries {
|
||||
self.entries.removeFirst(self.entries.count - self.maxEntries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clear() {
|
||||
DispatchQueue.main.async {
|
||||
self.entries.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all entries as a single string for clipboard
|
||||
var allText: String {
|
||||
entries.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
|
@ -11,15 +11,17 @@
|
|||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Payfrit Beacon</string>
|
||||
<string>$(APP_DISPLAY_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Payfrit Beacon</string>
|
||||
<string>$(APP_DISPLAY_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>Payfrit Beacon uses Bluetooth to discover and configure nearby beacons.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Payfrit Beacon uses Face ID for quick sign-in.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ struct PayfritBeaconApp: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
.preferredColorScheme(.light)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import SwiftUI
|
|||
struct ScanView: View {
|
||||
let businessId: Int
|
||||
let businessName: String
|
||||
var reprovisionServicePoint: ServicePoint? = nil // If set, we're re-provisioning an existing SP
|
||||
var onBack: () -> Void
|
||||
|
||||
@StateObject private var bleScanner = BLEBeaconScanner()
|
||||
@StateObject private var provisioner = BeaconProvisioner()
|
||||
|
||||
@State private var namespace: BusinessNamespace?
|
||||
@State private var servicePoints: [ServicePoint] = []
|
||||
@State private var nextTableNumber: Int = 1
|
||||
@State private var provisionedCount: Int = 0
|
||||
|
|
@ -21,6 +23,18 @@ struct ScanView: View {
|
|||
@State private var assignName = ""
|
||||
@State private var isProvisioning = false
|
||||
@State private var provisioningProgress = ""
|
||||
@State private var provisioningError: String?
|
||||
|
||||
// Action sheet + check config
|
||||
@State private var showBeaconActionSheet = false
|
||||
@State private var showCheckConfigSheet = false
|
||||
@State private var checkConfigData: BeaconCheckResult?
|
||||
@State private var isCheckingConfig = false
|
||||
@State private var checkConfigError: String?
|
||||
|
||||
// Debug log
|
||||
@State private var showDebugLog = false
|
||||
@ObservedObject private var debugLog = DebugLog.shared
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -38,6 +52,13 @@ struct ScanView: View {
|
|||
.font(.caption)
|
||||
.foregroundColor(.payfritGreen)
|
||||
}
|
||||
Button {
|
||||
showDebugLog = true
|
||||
} label: {
|
||||
Image(systemName: "ladybug")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
|
|
@ -48,10 +69,16 @@ struct ScanView: View {
|
|||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
if let sp = reprovisionServicePoint {
|
||||
Text("Re-provision: \(sp.name)")
|
||||
.font(.subheadline.bold())
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Next: Table \(nextTableNumber)")
|
||||
.font(.subheadline.bold())
|
||||
.foregroundColor(.payfritGreen)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
|
|
@ -90,7 +117,10 @@ struct ScanView: View {
|
|||
LazyVStack(spacing: 8) {
|
||||
ForEach(bleScanner.discoveredBeacons) { beacon in
|
||||
beaconRow(beacon)
|
||||
.onTapGesture { selectBeacon(beacon) }
|
||||
.onTapGesture {
|
||||
selectedBeacon = beacon
|
||||
showBeaconActionSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
@ -133,6 +163,33 @@ struct ScanView: View {
|
|||
.modifier(DevBanner())
|
||||
.overlay(snackOverlay, alignment: .bottom)
|
||||
.sheet(isPresented: $showAssignSheet) { assignSheet }
|
||||
.sheet(isPresented: $showCheckConfigSheet) { checkConfigSheet }
|
||||
.sheet(isPresented: $showDebugLog) { debugLogSheet }
|
||||
.confirmationDialog(
|
||||
selectedBeacon?.displayName ?? "Beacon",
|
||||
isPresented: $showBeaconActionSheet,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
if let sp = reprovisionServicePoint {
|
||||
Button("Provision for \(sp.name)") {
|
||||
guard selectedBeacon != nil else { return }
|
||||
reprovisionBeacon()
|
||||
}
|
||||
} else {
|
||||
Button("Configure (Assign & Provision)") {
|
||||
guard selectedBeacon != nil else { return }
|
||||
assignName = "Table \(nextTableNumber)"
|
||||
showAssignSheet = true
|
||||
}
|
||||
}
|
||||
Button("Check Current Config") {
|
||||
guard let beacon = selectedBeacon else { return }
|
||||
checkConfig(beacon)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
selectedBeacon = nil
|
||||
}
|
||||
}
|
||||
.onAppear { loadServicePoints() }
|
||||
}
|
||||
|
||||
|
|
@ -179,13 +236,15 @@ struct ScanView: View {
|
|||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if beacon.type != .unknown {
|
||||
Text(beacon.type.rawValue)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(beacon.type == .kbeacon ? Color.blue : Color.purple)
|
||||
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
|
||||
Text("\(beacon.rssi) dBm")
|
||||
.font(.caption)
|
||||
|
|
@ -230,7 +289,7 @@ struct ScanView: View {
|
|||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(beacon.type == .kbeacon ? Color.blue : Color.purple)
|
||||
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
Text("Signal: \(beacon.rssi) dBm")
|
||||
|
|
@ -251,19 +310,6 @@ struct ScanView: View {
|
|||
.font(.title3)
|
||||
}
|
||||
|
||||
// KBeacon warning
|
||||
if beacon.type == .kbeacon {
|
||||
HStack {
|
||||
Image(systemName: "info.circle.fill")
|
||||
.foregroundColor(.infoBlue)
|
||||
Text("KBeacon requires their app for provisioning. Config will be copied to clipboard.")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.infoBlue.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Provisioning progress
|
||||
if isProvisioning {
|
||||
HStack {
|
||||
|
|
@ -277,6 +323,20 @@ struct ScanView: View {
|
|||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Provisioning error
|
||||
if let error = provisioningError {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.red)
|
||||
Text(error)
|
||||
.font(.callout)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.red.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
|
@ -303,6 +363,270 @@ struct ScanView: View {
|
|||
.interactiveDismissDisabled(isProvisioning)
|
||||
}
|
||||
|
||||
// MARK: - Check Config Sheet
|
||||
|
||||
private var checkConfigSheet: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if let beacon = selectedBeacon {
|
||||
// Beacon identity
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Beacon")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Text(beacon.displayName)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if beacon.type != .unknown {
|
||||
Text(beacon.type.rawValue)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(beacon.type == .kbeacon ? Color.blue : beacon.type == .dxsmart ? Color.orange : Color.purple)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
Text("Signal: \(beacon.rssi) dBm")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
|
||||
// Loading state
|
||||
if isCheckingConfig {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(provisioner.progress.isEmpty ? "Connecting..." : provisioner.progress)
|
||||
.font(.callout)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if let error = checkConfigError {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text(error)
|
||||
.font(.callout)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Parsed config data
|
||||
if let data = checkConfigData {
|
||||
// iBeacon configuration
|
||||
if data.hasConfig {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("iBeacon Configuration")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
|
||||
if let uuid = data.uuid {
|
||||
configRow("UUID", uuid)
|
||||
}
|
||||
if let major = data.major {
|
||||
configRow("Major", "\(major)")
|
||||
}
|
||||
if let minor = data.minor {
|
||||
configRow("Minor", "\(minor)")
|
||||
}
|
||||
if let name = data.deviceName {
|
||||
configRow("Name", name)
|
||||
}
|
||||
if let rssi = data.rssiAt1m {
|
||||
configRow("RSSI@1m", "\(rssi) dBm")
|
||||
}
|
||||
if let interval = data.advInterval {
|
||||
configRow("Interval", "\(interval)00 ms")
|
||||
}
|
||||
if let tx = data.txPower {
|
||||
configRow("TX Power", "\(tx)")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Device info
|
||||
if data.battery != nil || data.macAddress != nil {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Device Info")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
|
||||
if let battery = data.battery {
|
||||
configRow("Battery", "\(battery)%")
|
||||
}
|
||||
if let mac = data.macAddress {
|
||||
configRow("MAC", mac)
|
||||
}
|
||||
if let slots = data.frameSlots {
|
||||
let slotStr = slots.enumerated().map { i, s in
|
||||
"Slot\(i): 0x\(String(format: "%02X", s))"
|
||||
}.joined(separator: " ")
|
||||
configRow("Frames", slotStr)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// No config found
|
||||
if !data.hasConfig && data.battery == nil && data.macAddress == nil && data.rawResponses.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.secondary)
|
||||
Text("No DX-Smart config data received")
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if !data.servicesFound.isEmpty {
|
||||
Text("Services: \(data.servicesFound.joined(separator: ", "))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Raw responses (debug section)
|
||||
if !data.rawResponses.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("Raw Responses")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Button {
|
||||
let raw = data.rawResponses.joined(separator: "\n")
|
||||
UIPasteboard.general.string = raw
|
||||
showSnack("Copied to clipboard")
|
||||
} label: {
|
||||
Image(systemName: "doc.on.doc")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
Text(data.rawResponses.joined(separator: "\n"))
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Services/chars discovery info
|
||||
if !data.characteristicsFound.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("BLE Discovery")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(data.characteristicsFound.joined(separator: "\n"))
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("No beacon selected")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Check Config")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
showCheckConfigSheet = false
|
||||
selectedBeacon = nil
|
||||
checkConfigData = nil
|
||||
checkConfigError = nil
|
||||
}
|
||||
.disabled(isCheckingConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.interactiveDismissDisabled(isCheckingConfig)
|
||||
}
|
||||
|
||||
private func configRow(_ label: String, _ value: String) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: 80, alignment: .leading)
|
||||
Text(value)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Debug Log Sheet
|
||||
|
||||
private var debugLogSheet: some View {
|
||||
NavigationStack {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(Array(debugLog.entries.enumerated()), id: \.offset) { idx, entry in
|
||||
Text(entry)
|
||||
.font(.system(.caption2, design: .monospaced))
|
||||
.textSelection(.enabled)
|
||||
.id(idx)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
.onAppear {
|
||||
if let last = debugLog.entries.indices.last {
|
||||
proxy.scrollTo(last, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Debug Log")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") { showDebugLog = false }
|
||||
}
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
UIPasteboard.general.string = debugLog.allText
|
||||
} label: {
|
||||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
Button {
|
||||
debugLog.clear()
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Snack Overlay
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -333,7 +657,16 @@ struct ScanView: View {
|
|||
private func loadServicePoints() {
|
||||
Task {
|
||||
do {
|
||||
servicePoints = try await Api.shared.listServicePoints(businessId: businessId)
|
||||
// Load namespace (UUID + Major) and service points in parallel
|
||||
async let nsTask = Api.shared.allocateBusinessNamespace(businessId: businessId)
|
||||
async let spTask = Api.shared.listServicePoints(businessId: businessId)
|
||||
|
||||
namespace = try await nsTask
|
||||
servicePoints = try await spTask
|
||||
|
||||
DebugLog.shared.log("[ScanView] Loaded namespace: uuid=\(namespace?.uuid ?? "nil") major=\(namespace?.major ?? 0)")
|
||||
DebugLog.shared.log("[ScanView] Loaded \(servicePoints.count) service points")
|
||||
|
||||
// Find next table number
|
||||
let maxNumber = servicePoints.compactMap { sp -> Int? in
|
||||
guard let match = sp.name.range(of: #"Table\s+(\d+)"#,
|
||||
|
|
@ -345,7 +678,7 @@ struct ScanView: View {
|
|||
}.max() ?? 0
|
||||
nextTableNumber = maxNumber + 1
|
||||
} catch {
|
||||
// Silently continue
|
||||
DebugLog.shared.log("[ScanView] loadServicePoints error: \(error)")
|
||||
}
|
||||
|
||||
// Auto-start scan
|
||||
|
|
@ -361,10 +694,103 @@ struct ScanView: View {
|
|||
bleScanner.startScanning()
|
||||
}
|
||||
|
||||
private func selectBeacon(_ beacon: DiscoveredBeacon) {
|
||||
selectedBeacon = beacon
|
||||
assignName = "Table \(nextTableNumber)"
|
||||
showAssignSheet = true
|
||||
private func checkConfig(_ beacon: DiscoveredBeacon) {
|
||||
isCheckingConfig = true
|
||||
checkConfigError = nil
|
||||
checkConfigData = nil
|
||||
showCheckConfigSheet = true
|
||||
|
||||
// Stop scanning to avoid BLE interference
|
||||
bleScanner.stopScanning()
|
||||
|
||||
provisioner.readConfig(beacon: beacon) { data, error in
|
||||
Task { @MainActor in
|
||||
isCheckingConfig = false
|
||||
if let data = data {
|
||||
checkConfigData = data
|
||||
}
|
||||
if let error = error {
|
||||
checkConfigError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reprovisionBeacon() {
|
||||
guard let beacon = selectedBeacon, let sp = reprovisionServicePoint else { return }
|
||||
|
||||
guard let ns = namespace else {
|
||||
failProvisioning("Namespace not loaded — go back and try again")
|
||||
return
|
||||
}
|
||||
|
||||
// Stop scanning
|
||||
bleScanner.stopScanning()
|
||||
|
||||
isProvisioning = true
|
||||
provisioningProgress = "Preparing..."
|
||||
provisioningError = nil
|
||||
showAssignSheet = true // Reuse assign sheet to show progress
|
||||
assignName = sp.name
|
||||
|
||||
DebugLog.shared.log("[ScanView] reprovisionBeacon: sp=\(sp.name) spId=\(sp.servicePointId) minor=\(String(describing: sp.beaconMinor)) beacon=\(beacon.displayName)")
|
||||
|
||||
Task {
|
||||
do {
|
||||
// If SP has no minor, re-fetch to get it
|
||||
var minor = sp.beaconMinor
|
||||
if minor == nil {
|
||||
DebugLog.shared.log("[ScanView] reprovisionBeacon: SP has no minor, re-fetching...")
|
||||
let refreshed = try await Api.shared.listServicePoints(businessId: businessId)
|
||||
minor = refreshed.first(where: { $0.servicePointId == sp.servicePointId })?.beaconMinor
|
||||
}
|
||||
|
||||
guard let beaconMinor = minor else {
|
||||
failProvisioning("Service point has no beacon minor assigned")
|
||||
return
|
||||
}
|
||||
|
||||
// Build config from namespace + service point (uuidClean for BLE)
|
||||
let deviceName = "PF-\(sp.name)"
|
||||
let beaconConfig = BeaconConfig(
|
||||
uuid: ns.uuidClean,
|
||||
major: ns.major,
|
||||
minor: beaconMinor,
|
||||
txPower: -59,
|
||||
interval: 350,
|
||||
deviceName: deviceName
|
||||
)
|
||||
|
||||
DebugLog.shared.log("[ScanView] reprovisionBeacon: BLE uuid=\(ns.uuidClean) API uuid=\(ns.uuid) major=\(ns.major) minor=\(beaconMinor)")
|
||||
provisioningProgress = "Provisioning beacon..."
|
||||
|
||||
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
||||
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||
Task { @MainActor in
|
||||
switch result {
|
||||
case .success:
|
||||
do {
|
||||
try await Api.shared.registerBeaconHardware(
|
||||
businessId: businessId,
|
||||
servicePointId: sp.servicePointId,
|
||||
uuid: ns.uuid,
|
||||
major: ns.major,
|
||||
minor: beaconMinor,
|
||||
hardwareId: hardwareId
|
||||
)
|
||||
finishProvisioning(name: sp.name)
|
||||
} catch {
|
||||
failProvisioning(error.localizedDescription)
|
||||
}
|
||||
case .failure(let error):
|
||||
failProvisioning(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
failProvisioning(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveBeacon() {
|
||||
|
|
@ -372,65 +798,78 @@ struct ScanView: View {
|
|||
let name = assignName.trimmingCharacters(in: .whitespaces)
|
||||
guard !name.isEmpty else { return }
|
||||
|
||||
guard let ns = namespace else {
|
||||
failProvisioning("Namespace not loaded — go back and try again")
|
||||
return
|
||||
}
|
||||
|
||||
isProvisioning = true
|
||||
provisioningProgress = "Creating service point..."
|
||||
provisioningProgress = "Preparing..."
|
||||
provisioningError = nil
|
||||
|
||||
// Stop scanning to avoid BLE interference
|
||||
bleScanner.stopScanning()
|
||||
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: name=\(name) beacon=\(beacon.displayName) businessId=\(businessId)")
|
||||
|
||||
Task {
|
||||
do {
|
||||
// 1. Create or get service point
|
||||
let servicePoint = try await Api.shared.createServicePoint(businessId: businessId, name: name)
|
||||
provisioningProgress = "Getting beacon config..."
|
||||
// 1. Reuse existing service point if name matches, otherwise create new
|
||||
var servicePoint: ServicePoint
|
||||
if let existing = servicePoints.first(where: { $0.name.caseInsensitiveCompare(name) == .orderedSame }) {
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: found existing SP id=\(existing.servicePointId) minor=\(String(describing: existing.beaconMinor))")
|
||||
servicePoint = existing
|
||||
} else {
|
||||
provisioningProgress = "Creating service point..."
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: creating new SP...")
|
||||
servicePoint = try await Api.shared.createServicePoint(businessId: businessId, name: name)
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: created SP id=\(servicePoint.servicePointId) minor=\(String(describing: servicePoint.beaconMinor))")
|
||||
}
|
||||
|
||||
// 2. Get beacon config from backend
|
||||
let config = try await Api.shared.getBeaconConfig(businessId: businessId, servicePointId: servicePoint.servicePointId)
|
||||
// If SP has no minor yet, re-fetch service points to get the allocated minor
|
||||
if servicePoint.beaconMinor == nil {
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: SP has no minor, re-fetching service points...")
|
||||
let refreshed = try await Api.shared.listServicePoints(businessId: businessId)
|
||||
if let updated = refreshed.first(where: { $0.servicePointId == servicePoint.servicePointId }) {
|
||||
servicePoint = updated
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: refreshed SP minor=\(String(describing: servicePoint.beaconMinor))")
|
||||
}
|
||||
}
|
||||
|
||||
guard let minor = servicePoint.beaconMinor else {
|
||||
failProvisioning("Service point has no beacon minor assigned")
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Build config from namespace + service point (uuidClean = no dashes, for BLE)
|
||||
let deviceName = "PF-\(name)"
|
||||
let beaconConfig = BeaconConfig(
|
||||
uuid: ns.uuidClean,
|
||||
major: ns.major,
|
||||
minor: minor,
|
||||
txPower: -59,
|
||||
interval: 350,
|
||||
deviceName: deviceName
|
||||
)
|
||||
|
||||
DebugLog.shared.log("[ScanView] saveBeacon: BLE uuid=\(ns.uuidClean) API uuid=\(ns.uuid) major=\(ns.major) minor=\(minor)")
|
||||
provisioningProgress = "Provisioning beacon..."
|
||||
|
||||
// 3. Provision the beacon
|
||||
let beaconConfig = BeaconConfig(
|
||||
uuid: config.uuid,
|
||||
major: config.major,
|
||||
minor: config.minor,
|
||||
txPower: Int8(config.txPower),
|
||||
interval: UInt16(config.interval)
|
||||
)
|
||||
|
||||
if beacon.type == .kbeacon {
|
||||
// Copy config to clipboard for KBeacon app
|
||||
let clipboardText = """
|
||||
UUID: \(formatUuidWithDashes(config.uuid))
|
||||
Major: \(config.major)
|
||||
Minor: \(config.minor)
|
||||
TxPower: \(config.txPower)
|
||||
"""
|
||||
UIPasteboard.general.string = clipboardText
|
||||
showSnack("Config copied! Use KBeacon app to program.")
|
||||
|
||||
// Register in backend
|
||||
try await Api.shared.registerBeaconHardware(
|
||||
businessId: businessId,
|
||||
servicePointId: servicePoint.servicePointId,
|
||||
uuid: config.uuid,
|
||||
major: config.major,
|
||||
minor: config.minor,
|
||||
macAddress: nil
|
||||
)
|
||||
|
||||
finishProvisioning(name: name)
|
||||
} else {
|
||||
// BlueCharm - provision directly via GATT
|
||||
// 3. Provision the beacon via GATT
|
||||
let hardwareId = beacon.id.uuidString // BLE peripheral identifier
|
||||
provisioner.provision(beacon: beacon, config: beaconConfig) { result in
|
||||
Task { @MainActor in
|
||||
switch result {
|
||||
case .success:
|
||||
// Register in backend
|
||||
// Register in backend (use original UUID from API, not cleaned)
|
||||
do {
|
||||
try await Api.shared.registerBeaconHardware(
|
||||
businessId: businessId,
|
||||
servicePointId: servicePoint.servicePointId,
|
||||
uuid: config.uuid,
|
||||
major: config.major,
|
||||
minor: config.minor,
|
||||
macAddress: nil
|
||||
uuid: ns.uuid,
|
||||
major: ns.major,
|
||||
minor: minor,
|
||||
hardwareId: hardwareId
|
||||
)
|
||||
finishProvisioning(name: name)
|
||||
} catch {
|
||||
|
|
@ -441,7 +880,6 @@ struct ScanView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
failProvisioning(error.localizedDescription)
|
||||
}
|
||||
|
|
@ -476,9 +914,10 @@ struct ScanView: View {
|
|||
}
|
||||
|
||||
private func failProvisioning(_ error: String) {
|
||||
DebugLog.shared.log("[ScanView] Provisioning failed: \(error)")
|
||||
isProvisioning = false
|
||||
provisioningProgress = ""
|
||||
showSnack("Error: \(error)")
|
||||
provisioningError = error
|
||||
}
|
||||
|
||||
private func formatUuidWithDashes(_ raw: String) -> String {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ struct ServicePointListView: View {
|
|||
@State private var newServicePointName = ""
|
||||
@State private var isAdding = false
|
||||
|
||||
// Beacon scan
|
||||
@State private var showScanView = false
|
||||
|
||||
// Re-provision existing service point
|
||||
@State private var tappedServicePoint: ServicePoint?
|
||||
@State private var reprovisionServicePoint: ServicePoint?
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -94,14 +101,22 @@ struct ServicePointListView: View {
|
|||
.padding(.vertical, 20)
|
||||
} else {
|
||||
ForEach(servicePoints) { sp in
|
||||
Button {
|
||||
tappedServicePoint = sp
|
||||
} label: {
|
||||
HStack {
|
||||
Text(sp.name)
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
if let minor = sp.beaconMinor {
|
||||
Text("Minor: \(minor)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +132,12 @@ struct ServicePointListView: View {
|
|||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Back", action: onBack)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showScanView = true
|
||||
} label: {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
Button {
|
||||
showAddSheet = true
|
||||
} label: {
|
||||
|
|
@ -128,6 +148,49 @@ struct ServicePointListView: View {
|
|||
}
|
||||
.onAppear { loadData() }
|
||||
.sheet(isPresented: $showAddSheet) { addServicePointSheet }
|
||||
.fullScreenCover(isPresented: $showScanView) {
|
||||
ScanView(
|
||||
businessId: businessId,
|
||||
businessName: businessName,
|
||||
onBack: {
|
||||
showScanView = false
|
||||
loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
.fullScreenCover(item: $reprovisionServicePoint) { sp in
|
||||
ScanView(
|
||||
businessId: businessId,
|
||||
businessName: businessName,
|
||||
reprovisionServicePoint: sp,
|
||||
onBack: {
|
||||
reprovisionServicePoint = nil
|
||||
loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
.confirmationDialog(
|
||||
tappedServicePoint?.name ?? "Service Point",
|
||||
isPresented: Binding(
|
||||
get: { tappedServicePoint != nil },
|
||||
set: { if !$0 { tappedServicePoint = nil } }
|
||||
),
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Re-provision Beacon") {
|
||||
reprovisionServicePoint = tappedServicePoint
|
||||
tappedServicePoint = nil
|
||||
}
|
||||
Button("Delete", role: .destructive) {
|
||||
if let sp = tappedServicePoint {
|
||||
deleteServicePoint(sp)
|
||||
}
|
||||
tappedServicePoint = nil
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
tappedServicePoint = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Add Sheet
|
||||
|
|
@ -227,6 +290,17 @@ struct ServicePointListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func deleteServicePoint(_ sp: ServicePoint) {
|
||||
Task {
|
||||
do {
|
||||
try await Api.shared.deleteServicePoint(businessId: businessId, servicePointId: sp.servicePointId)
|
||||
servicePoints.removeAll { $0.servicePointId == sp.servicePointId }
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addServicePoint() {
|
||||
let name = newServicePointName.trimmingCharacters(in: .whitespaces)
|
||||
guard !name.isEmpty else { return }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue