This commit is contained in:
袁智鸿
2026-01-07 17:59:49 +08:00
parent 35bd1e0898
commit 756a998a3f
60 changed files with 9068 additions and 3 deletions
+2 -1
View File
@@ -1 +1,2 @@
.DS_Store
.DS_Store
.idea/
+62 -1
View File
@@ -1 +1,62 @@
# StackChan App
# Project Setup and Running Instructions
## 1. Clone the repository
```bash
git clone https://github.com/m5stack/StackChan
cd StackChan/app
```
## 2. Open the project in Xcode
Open the project in Xcode:
Doubleclick the `.xcodeproj` file, or open Xcode → File → Open, then select the project.
1. Select your target device or simulator.
### Connect an iPhone (Optional but Recommended)
- Connect your iPhone to the Mac using a USB cable.
- Unlock the iPhone and tap **Trust This Computer** if prompted.
- In Xcode, select your iPhone as the run destination at the top.
### Enable Developer Mode on iPhone (iOS 16+)
> **Important:** Developer Mode will only appear after the iPhone has been connected to Xcode at least once.
If you do not see this option, make sure your iPhone is connected to the Mac, unlocked, trusted, and recognized by Xcode.
- On the iPhone, go to **Settings → Privacy & Security → Developer Mode**.
- Turn on Developer Mode and restart the iPhone.
- After restart, confirm enabling Developer Mode.
## 3. Configure Signing & Capabilities
This step allows Xcode to install the app on your iPhone.
1. In Xcode, select the project in the left sidebar.
2. Select the app target.
3. Open the **Signing & Capabilities** tab.
4. Sign in with your Apple ID (Xcode → Settings → Accounts → Add Apple ID).
5. Set **Team** to your Apple ID.
6. Change **Bundle Identifier** to a unique value, for example:
`com.yourname.stackchan`
7. Ensure no red error messages remain.
> **Note:** A free Apple ID is sufficient for testing on your own iPhone.
## 4. Modify network configuration
Before running the app, you need to set the correct server IP:
1. Open the file `Network/Urls.swift`.
2. Find the line defining the base URL, for example:
```swift
// Base URL configured according to the server's IP
static let url = "192.168.51.24:12800/"
```
3. Replace the IP address (`192.168.51.24`) with the IP of the computer where the server is running.
4. Save the file.
## 5. Run the project
Press `Cmd + R` to build and run the app.
> **Note:** The first build may take several minutes as Xcode prepares the environment.
If running on an iPhone for the first time, you may need to trust yourself as a developer:
- On your iPhone, go to **Settings → General → VPN & Device Management → Trust Developer** and trust the developer profile that appears.
The app will now connect to the server at the IP you configured.
+404
View File
@@ -0,0 +1,404 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
0E4478B92F0A538600010197 /* README.MD in Resources */ = {isa = PBXBuildFile; fileRef = 0E4478B82F0A538600010197 /* README.MD */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
0E4478B82F0A538600010197 /* README.MD */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.MD; sourceTree = "<group>"; };
0EBD7D382ECDA27C0001A9D1 /* StackChan.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StackChan.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
0EBD7E222ECDC9510001A9D1 /* Exceptions for "StackChan" folder in "StackChan" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 0EBD7D372ECDA27C0001A9D1 /* StackChan */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
0EBD7E222ECDC9510001A9D1 /* Exceptions for "StackChan" folder in "StackChan" target */,
);
explicitFileTypes = {
Info.plist = text.xml;
};
path = StackChan;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
0EBD7D352ECDA27C0001A9D1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0EBD7D2F2ECDA27C0001A9D1 = {
isa = PBXGroup;
children = (
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */,
0EBD7D392ECDA27C0001A9D1 /* Products */,
0E4478B82F0A538600010197 /* README.MD */,
);
sourceTree = "<group>";
};
0EBD7D392ECDA27C0001A9D1 /* Products */ = {
isa = PBXGroup;
children = (
0EBD7D382ECDA27C0001A9D1 /* StackChan.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0EBD7D372ECDA27C0001A9D1 /* StackChan */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0EBD7D432ECDA27D0001A9D1 /* Build configuration list for PBXNativeTarget "StackChan" */;
buildPhases = (
0EBD7D342ECDA27C0001A9D1 /* Sources */,
0EBD7D352ECDA27C0001A9D1 /* Frameworks */,
0EBD7D362ECDA27C0001A9D1 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
0EBD7D3A2ECDA27C0001A9D1 /* StackChan */,
);
name = StackChan;
packageProductDependencies = (
);
productName = StackChan;
productReference = 0EBD7D382ECDA27C0001A9D1 /* StackChan.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
0EBD7D302ECDA27C0001A9D1 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2610;
LastUpgradeCheck = 2620;
TargetAttributes = {
0EBD7D372ECDA27C0001A9D1 = {
CreatedOnToolsVersion = 26.1.1;
};
};
};
buildConfigurationList = 0EBD7D332ECDA27C0001A9D1 /* Build configuration list for PBXProject "StackChan" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 0EBD7D2F2ECDA27C0001A9D1;
minimizedProjectReferenceProxies = 1;
packageReferences = (
);
preferredProjectObjectVersion = 77;
productRefGroup = 0EBD7D392ECDA27C0001A9D1 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
0EBD7D372ECDA27C0001A9D1 /* StackChan */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0EBD7D362ECDA27C0001A9D1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0E4478B92F0A538600010197 /* README.MD in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
0EBD7D342ECDA27C0001A9D1 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
0EBD7D412ECDA27D0001A9D1 /* Debug */ = {
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;
DEVELOPMENT_TEAM = NG678HLKHZ;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
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 = 26.1;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
0EBD7D422ECDA27D0001A9D1 /* Release */ = {
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";
DEVELOPMENT_TEAM = NG678HLKHZ;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
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 = 26.1;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
0EBD7D442ECDA27D0001A9D1 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 0EBD7D3A2ECDA27C0001A9D1 /* StackChan */;
baseConfigurationReferenceRelativePath = App.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = StackChan/StackChan.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = NG678HLKHZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = StackChan/Info.plist;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth permission is required to connect to nearby devices";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission is required to scan the code";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is required to discover devices";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 1.0.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
0EBD7D452ECDA27D0001A9D1 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 0EBD7D3A2ECDA27C0001A9D1 /* StackChan */;
baseConfigurationReferenceRelativePath = App.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = StackChan/StackChan.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = NG678HLKHZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = StackChan/Info.plist;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Bluetooth permission is required to connect to nearby devices";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera permission is required to scan the code";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is required to discover devices";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Location permission is required to access Wi-Fi information";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
MARKETING_VERSION = 1.0.3;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = com.m5stack.StackChan;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
0EBD7D332ECDA27C0001A9D1 /* Build configuration list for PBXProject "StackChan" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0EBD7D412ECDA27D0001A9D1 /* Debug */,
0EBD7D422ECDA27D0001A9D1 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0EBD7D432ECDA27D0001A9D1 /* Build configuration list for PBXNativeTarget "StackChan" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0EBD7D442ECDA27D0001A9D1 /* Debug */,
0EBD7D452ECDA27D0001A9D1 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 0EBD7D302ECDA27C0001A9D1 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EBD7D372ECDA27C0001A9D1"
BuildableName = "StackChan.app"
BlueprintName = "StackChan"
ReferencedContainer = "container:StackChan.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
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 = "0EBD7D372ECDA27C0001A9D1"
BuildableName = "StackChan.app"
BlueprintName = "StackChan"
ReferencedContainer = "container:StackChan.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0EBD7D372ECDA27C0001A9D1"
BuildableName = "StackChan.app"
BlueprintName = "StackChan"
ReferencedContainer = "container:StackChan.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "632D2E91-955D-4E23-9652-CB7F63F6388B"
type = "1"
version = "2.0">
</Bucket>
@@ -0,0 +1,22 @@
<?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>SchemeUserState</key>
<dict>
<key>StackChan.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>0EBD7D372ECDA27C0001A9D1</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
Binary file not shown.
+268
View File
@@ -0,0 +1,268 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Combine
import SwiftUI
import CoreBluetooth
import ARKit
import Combine
enum PageType: Hashable {
case minicryEmotion
case cameraPage
case dance
}
class AppState: ObservableObject {
static let shared = AppState()
private init() {}
static let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
static var isRelease : Bool = true
@Published var showAlert: Bool = false
@Published var alertTitle: String = ""
var alertAction: (() -> Void)? = nil
func presentAlert(title: String,action: (() -> Void)? = nil) {
self.alertTitle = title
self.alertAction = action
self.showAlert = true
}
@AppStorage("deviceMac") var deviceMac: String = ""
@Published var stackChanPath: [PageType] = []
@Published var nearbyPath: [PageType] = []
@Published var settingsPath: [PageType] = []
@Published var showBindingDevice = false
@Published var forcedDisplayBindingDevice = true
@Published var showCjamgeNameAlert: Bool = false
@Published var showBindingDeviceAlert: Bool = false
@Published var newName: String = ""
@Published var deviceInfo: Device = Device()
let detector = DistanceDetector()
@Published var showSwitchFace: Bool = false
@Published var blufDeviceList: [BlufiDeviceInfo] = []
/// Whether currently pairing a device
@Published var showDeviceWifiSet = false
// Manual shutdown time, if just manually shut down, temporarily do not configure
var manualShutdownTime: Date? = nil
@Published var deviceIsOnline: Bool = false
func connectBulDevice(macAddress: String) {
if BlufiUtil.shared.blueSwitch {
BlufiUtil.shared.startScan()
} else {
BlufiUtil.shared.centralManagerDidUpdateState = { state in
switch state {
case .poweredOn:
BlufiUtil.shared.startScan()
default: break
}
}
}
BlufiUtil.shared.characteristicCallback = { characteristic in
if characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) {
if characteristic.uuid.uuidString == "E2E5E5E2-1234-5678-1234-56789ABCDEF0" {
BlufiUtil.shared.writeExpressionCharacteristic = characteristic
print("✏️ Expression writable characteristic assigned: \(characteristic.uuid)")
}
if characteristic.uuid.uuidString == "E2E5E5E1-1234-5678-1234-56789ABCDEF0" {
BlufiUtil.shared.writeHeadCharacteristic = characteristic
print("✏️ Head writable characteristic assigned: \(characteristic.uuid)")
}
}
}
}
func connectWebSocket() {
let webSocketUrl = Urls.getWebSocketUrl() + "?mac=" + deviceMac + "&deviceType=App&deviceId=" + AppState.deviceId
WebSocketUtil.shared.connect(urlString: webSocketUrl)
}
func sendWebSocketMessage(_ msgType: MsgType,_ data: Data? = nil) {
var buffer = Data([msgType.rawValue])
let payload = data ?? Data()
// payload length
let dataLen = UInt32(payload.count)
buffer.append(UInt8((dataLen >> 24) & 0xFF))
buffer.append(UInt8((dataLen >> 16) & 0xFF))
buffer.append(UInt8((dataLen >> 8) & 0xFF))
buffer.append(UInt8(dataLen & 0xFF))
// data
buffer.append(payload)
WebSocketUtil.shared.send(data: buffer)
}
/// Parse message data
func parseMessage(message: Data) -> (MsgType?,Data?) {
guard message.count >= 5 else {
return (nil,nil)
}
let typeByte = message[0]
guard let msgType = MsgType(rawValue: typeByte) else {
return (nil,nil)
}
let lengthData = message[1...4]
let dataLength = lengthData.reduce(0) { (result, byte) -> UInt32 in
return (result << 8) | UInt32(byte)
}
if message.count < 5 + Int(dataLength) {
return (nil,nil)
}
let payload = message[5..<(5 + Int(dataLength))]
return (msgType, Data(payload))
}
func updateDeviceInfo() {
let map = [
ValueConstant.mac: deviceMac,
ValueConstant.name: deviceInfo.name,
]
Networking.shared.put(pathUrl: Urls.deviceInfo, parameters: map) { result in
switch result {
case .success(let success):
do {
let response = try Response<String>.decode(from: success)
if response.isSuccess {
print("Update successful")
}
} catch {
print("Failed to parse data")
}
case .failure(let failure):
print("Request failed:", failure)
}
}
}
/// Distance detection, callback when close
func startDistanceDetection() {
detector.startDistanceDetection(
distanceUpdate: { distance in
let distanceInCm = distance * 100
if distanceInCm < 5 {
if self.showSwitchFace == false {
self.showSwitchFace = true
}
}
},
belowThreshold: {
// Execute your business logic
// For example: stop machine, send notification, etc.
}
)
}
func stopDistanceDetection() {
detector.stopDistanceDetection()
}
// Wrapper for ARSessionDelegate
class ARSessionDelegateWrapper: NSObject, ARSessionDelegate {
var onFrameUpdate: (ARFrame) -> Void
init(onFrameUpdate: @escaping (ARFrame) -> Void) {
self.onFrameUpdate = onFrameUpdate
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
onFrameUpdate(frame)
}
}
func getDeviceInfo() {
let map = [
ValueConstant.mac: deviceMac
]
Networking.shared.get(pathUrl: Urls.deviceInfo,parameters: map) { result in
switch result {
case .success(let success):
do {
let response = try Response<Device>.decode(from: success)
if response.isSuccess, let deviceInfo = response.data {
withAnimation {
self.deviceInfo = deviceInfo
self.newName = self.deviceInfo.name ?? ""
}
if deviceInfo.name == "" {
self.showCjamgeNameAlert = true
}
}
} catch {
print("Failed to parse data")
}
case .failure(let failure):
print("Request failed:", failure)
}
}
}
/// Enable Bluetooth functionality
func openBlufi() {
BlufiUtil.shared.blufDevicesMonitoring = { discovereDevices in
self.blufDeviceList = discovereDevices
// Check manualShutdownTime, if exists and not exceeding 5 seconds, temporarily do not show popup
if let shutdownTime = self.manualShutdownTime {
let timeInterval = Date().timeIntervalSince(shutdownTime)
if timeInterval < 5 {
return
}
}
if !self.showDeviceWifiSet {
if !self.blufDeviceList.isEmpty {
self.showDeviceWifiSet = true
}
}
}
}
// webSocket Message Monitoring
func webSocketMessageMonitoring() {
WebSocketUtil.shared.addObserver(for: "App") { (message: URLSessionWebSocketTask.Message) in
switch message {
case .data(let data):
let result = self.parseMessage(message: data)
if let msgType = result.0 {
switch msgType {
case MsgType.deviceOnline:
self.deviceIsOnline = false
case MsgType.deviceOffline:
self.deviceIsOnline = true
default:
break
}
}
case .string(let text):
print("Received a regular message: \(text)")
@unknown default:
break
}
}
}
}
@@ -0,0 +1,23 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.057",
"green" : "0.689",
"red" : "0.936"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"localizable" : true
}
}
@@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "app_logo.jpg",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "7.595.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "stackChanLogo.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

+29
View File
@@ -0,0 +1,29 @@
<?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>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSBonjourServices</key>
<array>
<string>_stackchan-mpc._tcp</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>This app requires access to the local network to communicate with devices.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to the microphone to capture audio data.</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDisplayName</key>
<string>StackChan World</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save the photo to the album</string>
</dict>
</plist>
+62
View File
@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
struct BlufiModel<T:Codable>: Codable {
var cmd: String? = nil
var data: T? = nil
func toJson() -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(self) else { return nil }
return String(data: jsonData, encoding: .utf8)
}
static func fromJson(_ json: String) -> BlufiModel<T>? {
guard let jsonData = json.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(BlufiModel<T>.self, from: jsonData)
}
}
struct BlufiWifi : Codable {
var ssid: String?
var password: String?
func toJson() -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(self) else { return nil }
return String(data: jsonData, encoding: .utf8)
}
static func fromJson(_ json: String) -> BlufiWifi? {
guard let jsonData = json.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(BlufiWifi.self, from: jsonData)
}
}
struct BlufiNotifyState : Codable {
var type: Int?
var state: String?
func toJson() -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(self) else { return nil }
return String(data: jsonData, encoding: .utf8)
}
static func fromJson(_ json: String) -> BlufiNotifyState? {
guard let jsonData = json.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(BlufiNotifyState.self, from: jsonData)
}
}

Some files were not shown because too many files have changed in this diff Show More