prepare v1.1.4 release with native bridge and stability cleanups (#38)

This commit is contained in:
小袁
2026-04-29 18:50:10 +08:00
committed by GitHub
parent d7ae8564c1
commit e8b9973544
257 changed files with 24601 additions and 8382 deletions
+361 -51
View File
@@ -1,62 +1,372 @@
# Project Setup and Running Instructions
# StackChan App
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Flutter](https://img.shields.io/badge/Flutter-3.0+-blue.svg)](https://flutter.dev/)
[![Dart](https://img.shields.io/badge/Dart-3.0+-blue.svg)](https://dart.dev/)
A powerful Flutter application for controlling and interacting with the StackChan AI robot companion. Features include
Bluetooth connectivity, AI conversation capabilities, facial expression rendering, and dance choreography.
## Features
- 🤖 **Bluetooth Device Management** - Connect and control StackChan robots via BLE
- 💬 **AI Conversation** - Natural language interaction powered by XiaoZhi AI
- 🎭 **Facial Expression Rendering** - Real-time 3D face animation using Three.js
- 🎵 **Music & Dance** - Create and play dance choreographies with music
- 📷 **Camera Integration** - AR features and face detection
- 🔐 **Secure Communication** - RSA encryption for data transmission
## System Requirements
- **Flutter SDK**: 3.0+
- **Dart SDK**: 3.0+
- **iOS**: 14.0+ (for iOS deployment)
- **Android**: API 21+ (for Android deployment)
- **macOS**: 11.0+ (for macOS deployment)
## Installation
### 1. Install Flutter
Follow the official Flutter installation guide for your operating system:
#### macOS
## 1. Clone the repository
```bash
git clone https://github.com/m5stack/StackChan
cd StackChan/app
# Download Flutter SDK
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
# Verify installation
flutter doctor
```
## 2. Open the project in Xcode
Open the project in Xcode:
#### Windows
Doubleclick the `.xcodeproj` file, or open Xcode → File → Open, then select the project.
```bash
# Download Flutter SDK from https://flutter.dev/docs/get-started/install/windows
# Extract and add to PATH
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/"
# Verify installation
flutter doctor
```
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.
#### Linux
> **Note:** The first build may take several minutes as Xcode prepares the environment.
```bash
# Download Flutter SDK
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
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.
# Install dependencies
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev
The app will now connect to the server at the IP you configured.
# Verify installation
flutter doctor
```
### 2. Set Up Project
```bash
# Clone the repository
git clone <repository-url>
cd StackChan
# Install dependencies
flutter pub get
```
### 3. Configure Backend Server
The application requires a backend server for full functionality. Configure the server endpoints before building:
#### Option A: Using Environment Configuration (Recommended)
Create a `.env` file in the project root or modify the configuration directly in code.
#### Option B: Direct Code Configuration
Modify `lib/network/urls.dart` to set your backend server URL:
```dart
// lib/network/urls.dart
class Urls {
// Update this to your backend server address
static const String url = "your-backend-server:port/";
// ... rest of the configuration
}
```
#### Option C: Configure Value Constants
Update `lib/util/value_constant.dart` for encryption keys and other constants:
```dart
// lib/util/value_constant.dart
class ValueConstant {
// Server RSA Public Key for encryption
static const String serverPublicKey = """
-----BEGIN PUBLIC KEY-----
YOUR_SERVER_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----
""";
// Client RSA Private Key for decryption
static const String clientPrivateKey = """
-----BEGIN RSA PRIVATE KEY-----
YOUR_CLIENT_PRIVATE_KEY_HERE
-----END RSA PRIVATE KEY-----
""";
}
```
**Important**: For production deployments, use environment variables or secure key management instead of hardcoding
keys.
## Building the Application
### iOS
```bash
# Install CocoaPods dependencies
cd ios
pod install
cd ..
# Run on iOS simulator
flutter run -d ios
# Build for release (iOS device)
flutter build ios --release
```
### Android
```bash
# Run on Android emulator or connected device
flutter run -d android
# Build APK for release
flutter build apk --release
# Build App Bundle for Google Play
flutter build appbundle --release
```
### Android Release Signing (JKS)
For release builds (`apk --release` / `appbundle --release`), configure a keystore instead of hardcoding passwords in
`build.gradle.kts`.
#### 1. Generate a JKS file
```bash
keytool -genkeypair -v \
-keystore android/app/release.jks \
-alias release \
-keyalg RSA -keysize 2048 -validity 10000
```
#### 2. Create `android/key.properties`
```properties
storePassword=YOUR_STORE_PASSWORD
keyPassword=YOUR_KEY_PASSWORD
keyAlias=release
storeFile=../app/release.jks
```
> `android/.gitignore` already ignores `key.properties` and `*.jks`. Keep these files private.
#### 3. Load the properties in `android/app/build.gradle.kts`
Use Kotlin DSL style configuration:
```kotlin
import java.util.Properties
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(keystorePropertiesFile.inputStream())
}
android {
signingConfigs {
create("release") {
if (keystorePropertiesFile.exists()) {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}
```
#### 4. Build release artifacts
```bash
flutter build apk --release
flutter build appbundle --release
```
#### 5. CI/CD recommendation
In CI, inject signing values through environment variables or secure secrets storage. Do not commit keystore passwords or
private keys to the repository.
## Project Structure
```
lib/
├── main.dart # Application entry point
├── app_state.dart # Global state management
├── model/ # Data models
│ ├── XiaoZhi/ # AI service models
│ ├── blue_device_info.dart # Bluetooth device models
│ ├── dance_list.dart # Dance choreography models
│ └── ...
├── network/ # Network layer
│ ├── http.dart # HTTP client with interceptors
│ ├── urls.dart # API endpoint configurations
│ └── web_socket_util.dart # WebSocket management
├── util/ # Utilities
│ ├── value_constant.dart # App constants and keys
│ ├── rsa_util.dart # RSA encryption/decryption
│ ├── blue_util.dart # Bluetooth utilities
│ ├── music_util.dart # Music and audio processing
│ └── ...
└── view/ # UI layer
├── home/ # Home screens
├── popup/ # Modal screens
└── util/ # UI components and widgets
```
## Backend API Integration
The application integrates with two main backend services:
### 1. StackChan Backend (`lib/network/urls.dart`)
- Device registration and management
- Dance choreography storage
- User authentication
- File upload and media management
### 2. XiaoZhi AI Service (`lib/util/XiaoZhi_util.dart`)
- AI conversation and chat functionality
- Agent management and configuration
- TTS (Text-to-Speech) voice selection
- License and activation management
**Base URL Configuration:**
- StackChan Backend: `http://<server-ip>:<port>/stackChan/`
- XiaoZhi AI: `https://XiaoZhi.me/`
## Development
### Code Style
This project follows the official Dart and Flutter style guidelines:
- Use `camelCase` for variables and functions
- Use `PascalCase` for classes and types
- Use `snake_case` for JSON keys (API communication)
- Document public APIs with doc comments
### Running Tests
```bash
# Run all tests
flutter test
# Run specific test file
flutter test test/widget_test.dart
# Run with coverage
flutter test --coverage
```
### Linting
```bash
# Run static analysis
flutter analyze
```
## Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Troubleshooting
### Common Issues
**1. Flutter doctor reports missing dependencies**
- Follow the instructions provided by `flutter doctor` to install missing components
- For iOS: Ensure Xcode is installed and command line tools are selected
- For Android: Ensure Android Studio and SDK are properly configured
**2. Bluetooth not working**
- Ensure Bluetooth permissions are granted
- For iOS: Check `NSBluetoothAlwaysUsageDescription` in Info.plist
- For Android: Check `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` permissions
**3. Backend connection fails**
- Verify server URL is correct in `lib/network/urls.dart`
- Check network connectivity
- Verify backend server is running and accessible
- Check SSL certificates for HTTPS connections
**4. Build fails on iOS**
```bash
cd ios
rm -rf Pods Podfile.lock
pod install --repo-update
cd ..
flutter clean
flutter pub get
```
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- M5Stack Technology CO LTD for the StackChan hardware
- Flutter team for the amazing framework
- Three.js for 3D rendering capabilities
- All contributors and open source libraries used in this project
## Support
For support, please:
1. Check the [Issues](../../issues) page for known problems
2. Create a new issue if your problem isn't already listed
3. For security issues, please contact security@m5stack.com directly
---
**Note**: This application requires compatible StackChan hardware and backend services for full functionality.
-404
View File
@@ -1,404 +0,0 @@
// !$*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 */;
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "632D2E91-955D-4E23-9652-CB7F63F6388B"
type = "1"
version = "2.0">
</Bucket>
@@ -1,22 +0,0 @@
<?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
@@ -1,268 +0,0 @@
/*
* 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
}
}
}
}
@@ -1,23 +0,0 @@
{
"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
}
}
@@ -1,36 +0,0 @@
{
"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
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "7.595.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
-29
View File
@@ -1,29 +0,0 @@
<?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
@@ -1,62 +0,0 @@
/*
* 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)
}
}
-12
View File
@@ -1,12 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
struct Device : Codable {
var mac: String = UUID().uuidString
var name: String? = nil
}
-39
View File
@@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
enum MsgType: UInt8, Codable {
case opus = 0x01
case jpeg = 0x02
case controlAvatar = 0x03
case controlMotion = 0x04
case onCamera = 0x05
case offCamera = 0x06
case textMessage = 0x07
case requestCall = 0x09
case refuseCall = 0x0A
case agreeCall = 0x0B
case hangupCall = 0x0C
case updateDeviceName = 0x0D
case getDeviceName = 0x0E
case ping = 0x10
case pong = 0x11
case onPhoneScreen = 0x12
case offPhoneScreen = 0x13
case dance = 0x14
case getAvatarPosture = 0x15
case deviceOffline = 0x16
case deviceOnline = 0x17
}
-32
View File
@@ -1,32 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
struct Post : Codable{
var id: Int
var mac: String? = nil
var name: String? = nil
var contentText: String? = nil
var contentImage: String? = nil
var createdAt: String? = nil
var postCommentList: [PostComment]? = nil
}
struct PostComment: Codable {
var id: Int? = nil
var postId: Int? = nil
var mac: String? = nil
var name: String? = nil
var content: String? = nil
var createAt: String? = nil
}
struct GetPostComment: Codable {
var list: [PostComment]? = nil
var total: Int? = nil
}
-79
View File
@@ -1,79 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
struct Response<T: Codable>: Codable {
let code: Int?
let message: String?
let data: T?
var isSuccess: Bool {
return code == 0
}
func unwrap(or defaultValue: T) -> T {
return data ?? defaultValue
}
static func decode(from jsonData: Data) throws -> Response<T> {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
return try decoder.decode(Response<T>.self, from: jsonData)
} catch let DecodingError.dataCorrupted(context) {
print("🔴 Data corrupted: \(context.debugDescription)")
printCodingPath(context.codingPath)
printJSON(jsonData)
throw DecodingError.dataCorrupted(context)
} catch let DecodingError.keyNotFound(key, context) {
print("🔴 Key '\(key.stringValue)' not found: \(context.debugDescription)")
printCodingPath(context.codingPath)
printJSON(jsonData)
throw DecodingError.keyNotFound(key, context)
} catch let DecodingError.typeMismatch(type, context) {
print("🔴 Type '\(type)' mismatch: \(context.debugDescription)")
printCodingPath(context.codingPath)
printJSON(jsonData)
throw DecodingError.typeMismatch(type, context)
} catch let DecodingError.valueNotFound(value, context) {
print("🔴 Value '\(value)' not found: \(context.debugDescription)")
printCodingPath(context.codingPath)
printJSON(jsonData)
throw DecodingError.valueNotFound(value, context)
} catch {
print("🔴 Other errors in the analysis: \(error)")
printJSON(jsonData)
throw error
}
}
static func decode(from json: [String: Any]) throws -> Response<T> {
let data = try JSONSerialization.data(withJSONObject: json, options: [])
return try decode(from: data)
}
func debugDescription() -> String {
return "Response(code: \(code ?? 0), message: \(message ?? ""), data: \(String(describing: data)))"
}
}
fileprivate func printCodingPath(_ codingPath: [CodingKey]) {
let path = codingPath.map { $0.stringValue }.joined(separator: ".")
print("📍 Error path: \(path)")
}
fileprivate func printJSON(_ data: Data) {
if let obj = try? JSONSerialization.jsonObject(with: data, options: []),
let prettyData = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]),
let str = String(data: prettyData, encoding: .utf8) {
print("📄 Original JSON:\n\(str)")
} else if let str = String(data: data, encoding: .utf8) {
print("📄 Original JSON:\n\(str)")
} else {
print("⚠️ Unable to parse the original JSON")
}
}
-9
View File
@@ -1,9 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
struct UploadFile : Codable {
var path: String? = nil
}
-328
View File
@@ -1,328 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
import Foundation
class Networking {
static let shared = Networking()
private init() {}
enum HTTPMethod: String {
case GET,POST,PUT,DELETE
}
private func request(
urlString: String,
method: HTTPMethod,
parameters: Any? = nil,
headers: [String: String] = [:],
completion: @escaping (Result<Data, Error>) -> Void
) {
var finalURLString = urlString
var httpBody: Data? = nil
if method == .GET {
if let params = parameters as? [String: Any], !params.isEmpty {
var components = URLComponents(string: urlString)
components?.queryItems = params.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
if let urlWithQuery = components?.url?.absoluteString {
finalURLString = urlWithQuery
}
}
} else {
if let params = parameters {
requestSetContentType: do {
requestSetBody: do {
do {
if let dict = params as? [String: Any] {
httpBody = try JSONSerialization.data(withJSONObject: dict, options: [])
} else if let array = params as? [Any] {
httpBody = try JSONSerialization.data(withJSONObject: array, options: [])
} else {
httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
}
} catch {
completion(.failure(error))
return
}
}
}
}
}
guard let url = URL(string: finalURLString) else {
completion(.failure(NSError(domain: "Invalid URL", code: -1)))
return
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if method != .GET, httpBody != nil {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody
}
setHandler(request: &request, headers: headers)
logRequest(request)
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data returned", code: -2)))
return
}
self.logResponse(data: data)
completion(.success(data))
}
}.resume()
}
func get(pathUrl: String, parameters: [String: Any] = [:], headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
request(urlString: finalUrl, method: .GET, parameters: parameters, headers: headers, completion: completion)
}
func post(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
request(urlString: finalUrl, method: .POST, parameters: parameters, headers: headers, completion: completion)
}
private func setHandler(request: inout URLRequest, headers: [String: String]) {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
if let token = UserDefaults.standard.string(forKey: ValueConstant.token), !token.isEmpty {
request.setValue(token, forHTTPHeaderField: ValueConstant.Authorization)
}
}
func postFromData(pathUrl: String,
parameters: [String: Any?] = [:],
headers: [String: String] = [:],
baseUrlString: String? = nil,
suffix:String? = nil,
completion: @escaping (Result<Data, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
guard let url = URL(string: finalUrl) else {
completion(.failure(NSError(domain: "Invalid URL", code: -1)))
return
}
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.POST.rawValue
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
setHandler(request: &request, headers: headers)
var requestBody = Data()
for (key, value) in parameters {
if let value = value {
if let fileData = value as? Data {
let type = mimeType(for: fileData)
let fileName = UUID().uuidString + (suffix ?? "")
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!)
requestBody.append("Content-Type: \(type)\r\n\r\n".data(using: .utf8)!)
requestBody.append(fileData)
requestBody.append("\r\n".data(using: .utf8)!)
} else if let array = value as? [Any] {
if let jsonData = try? JSONSerialization.data(withJSONObject: array, options: []) {
let jsonString = String(data: jsonData, encoding: .utf8)
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
requestBody.append("\(jsonString ?? "[]")\r\n".data(using: .utf8)!)
}
} else if let dict = value as? [String:Any] {
if let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []) {
let jsonString = String(data: jsonData, encoding: .utf8)
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
requestBody.append("\(jsonString ?? "{}")\r\n".data(using: .utf8)!)
}
} else {
let str = "\(value)"
requestBody.append("--\(boundary)\r\n".data(using: .utf8)!)
requestBody.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
requestBody.append("\(str)\r\n".data(using: .utf8)!)
}
}
}
requestBody.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = requestBody
logRequest(request)
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data returned", code: -2)))
return
}
self.logResponse(data: data)
completion(.success(data))
}
}.resume()
}
func anyToJson(data: Any) -> String {
func convert(_ value: Any) -> Any {
if let dict = value as? [String: Any] {
var newDict: [String: Any] = [:]
for (k, v) in dict {
newDict[k] = convert(v)
}
return newDict
} else if let dict = value as? [String: Any?] {
var newDict: [String: Any] = [:]
for (k, v) in dict {
if let unwrapped = v {
newDict[k] = convert(unwrapped)
} else {
newDict[k] = NSNull()
}
}
return newDict
} else if let array = value as? [Any] {
return array.map { convert($0) }
} else if let array = value as? [Any?] {
return array.map { $0 == nil ? NSNull() : convert($0!) }
} else if value is Int || value is Double || value is Bool || value is String {
return value
} else {
return "\(value)"
}
}
let converted = convert(data)
if JSONSerialization.isValidJSONObject(converted) {
do {
let jsonData = try JSONSerialization.data(withJSONObject: converted, options: [])
return String(data: jsonData, encoding: .utf8) ?? "[]"
} catch {
print("JSON Serialization error: \(error)")
return "[]"
}
} else {
if let str = converted as? String {
return "\"\(str)\""
} else {
return "\(converted)"
}
}
}
func put(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
request(urlString: finalUrl, method: .PUT, parameters: parameters, headers: headers, completion: completion)
}
func delete(pathUrl: String, parameters: Any? = nil, headers: [String: String] = [:], baseUrlString: String? = nil, completion: @escaping (Result<Data, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
request(urlString: finalUrl, method: .DELETE, parameters: parameters, headers: headers, completion: completion)
}
func download(pathUrl: String,
parameters: [String: Any] = [:],
headers: [String: String] = [:],
baseUrlString: String? = nil,
completion: @escaping (Result<String, Error>) -> Void) {
let finalUrl = (baseUrlString ?? Urls.getBaseUrl()) + pathUrl
let key = FileUtils.shared.hashedKey(for: finalUrl)
let cacheURL = FileUtils.shared.cacheDirectory().appendingPathComponent(key)
if FileManager.default.fileExists(atPath: cacheURL.path) {
completion(.success(cacheURL.path))
return
}
var request = URLRequest(url: URL(string: finalUrl)!)
request.httpMethod = "GET"
setHandler(request: &request, headers: headers)
URLSession.shared.downloadTask(with: request) { tempURL, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard let tempURL = tempURL else {
completion(.failure(NSError(domain: "No file downloaded", code: -3)))
return
}
do {
let directory = cacheURL.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: directory.path) {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
if FileManager.default.fileExists(atPath: cacheURL.path) {
try FileManager.default.removeItem(at: cacheURL)
}
try FileManager.default.moveItem(at: tempURL, to: cacheURL)
completion(.success(cacheURL.path))
} catch {
completion(.failure(error))
}
}
}.resume()
}
private func logRequest(_ request: URLRequest) {
print("➡️ Request URL: \(request.url?.absoluteString ?? "")")
print("➡️ Method: \(request.httpMethod ?? "")")
print("➡️ Headers: \(request.allHTTPHeaderFields ?? [:])")
if let body = request.httpBody {
if let bodyString = String(data: body, encoding: .utf8) {
print("➡️ Body:")
bodyString.jsonPrint()
} else {
let sizeInMB = Double(body.count) / (1024 * 1024)
print(String(format: "➡️ Body (binary data, size: %.2f MB)", sizeInMB))
}
}
}
private func logResponse(data: Data) {
if let responseString = String(data: data, encoding: .utf8) {
print("⬅️ Response:")
responseString.jsonPrint()
} else {
print("⬅️ Response (binary data, length: \(data.count) bytes)")
}
}
private func mimeType(for data: Data) -> String {
var bytes = [UInt8](repeating: 0, count: 1)
data.copyBytes(to: &bytes, count: 1)
switch bytes[0] {
case 0xFF: return "image/jpeg"
case 0x89: return "image/png"
case 0x47: return "image/gif"
case 0x25: return "application/pdf"
case 0x49, 0x4D: return "image/tiff"
default: return "application/octet-stream"
}
}
}
-45
View File
@@ -1,45 +0,0 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
struct Urls {
// Base URL configured according to the server's IP
static let url = "192.168.51.43:12800/"
static func getBaseUrl() -> String {
return "http://" + url + "stackChan/"
}
static func getFileUrl() -> String {
return "http://" + url
}
static func getWebSocketUrl() -> String {
return "ws://" + url + "stackChan/ws"
}
static let registerMac = "api/v2/device/registerMac"
static let dance = "dance"
static let deviceRandomList = "device/randomList"
static let uploadFile = "uploadFile"
static let postAdd = "post/add"
static let postGet = "post/get"
static let postDelete = "post/delete"
static let deviceInfo = "device/info"
static let postCommentCreate = "post/comment/create"
static let postCommentDelete = "post/comment/delete"
static let postCommentGet = "post/comment/get"
}

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