mirror of
https://github.com/m5stack/StackChan.git
synced 2026-05-20 11:49:45 -07:00
prepare v1.1.4 release with native bridge and stability cleanups (#38)
This commit is contained in:
+361
-51
@@ -1,62 +1,372 @@
|
||||
# Project Setup and Running Instructions
|
||||
# StackChan App
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://flutter.dev/)
|
||||
[](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
|
||||
|
||||
Double‑click 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.
|
||||
@@ -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 */;
|
||||
}
|
||||
BIN
Binary file not shown.
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "632D2E91-955D-4E23-9652-CB7F63F6388B"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
-22
@@ -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.
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
struct UploadFile : Codable {
|
||||
var path: String? = nil
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user