Merge pull request #55 from m5stack/main

update branch
This commit is contained in:
Jiangyy
2026-05-12 09:14:36 +08:00
committed by GitHub
425 changed files with 30939 additions and 9198 deletions
+21 -10
View File
@@ -1,21 +1,32 @@
# StackChan Open Source
# StackChan Open-Source
<img src="https://vm.pledgebox.com/uploads/20251229/5sSmfjGLj9YYIzfHOSXQm4K00adu4Gv1.png" width="60%">
<img src="https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1205/K151_stack_chan_main_pictures_01.webp" width="60%">
**Pre-order your StackChan**: https://m5stack.com/stackchan
Here are StackChan related open-source resources, including source code of the StackChan firmware, remote controller firmware, mobile app (iOS and Android), and server.
> The software development is still in progress. Final features and documentation may change. Thank you for your understanding.
Update of this repo could be a little late than the released firmware and mobile app.
----
<img src="https://cdn.shopify.com/s/files/1/0056/7689/2250/files/5a589623895f65487717894d9240f6b8.png" width="60%">
**StackChan is a super kawaii AI desktop robot co-created by M5Stack and the user community.** It uses the M5Stack **flagship IoT development kit [CoreS3](https://docs.m5stack.com/en/core/CoreS3)** as its main controller, powered by an ESP32-S3 SoC featuring a 240 MHz dual-core processor, with 16MB Flash and 8MB PSRAM onboard, and supporting Wi-Fi and BLE. The main unit also integrates a 2.0-inch capacitive touch display with a high-strength glass cover, a 0.3 MP camera, a proximity sensor, a 9-axis IMU (accelerometer + gyroscope + magnetometer), a microSD card slot, a 1W speaker, dual microphones, and power/reset buttons.
**StackChan is a super kawaii AI desktop robot co-created by M5Stack and the user community.** It uses the M5Stack **flagship IoT development kit [CoreS3](https://docs.m5stack.com/en/core/CoreS3)** as its main controller, powered by an ESP32-S3 SoC featuring a 240 MHz dual-core processor, with 16MB Flash and 8MB PSRAM onboard, and supporting Wi-Fi and BLE. The main unit also integrates a 2.0-inch capacitive touch display with a high-strength glass cover, a 0.3 MP camera, a proximity & ambient light sensor, a 9-axis IMU (accelerometer + gyroscope + magnetometer), a microSD card slot, a 1W speaker, dual microphones, and power/reset buttons.
The **robot body**, connected to the main unit, includes a USB-C interface for power and data, a 700 mAh battery, two feedback servos (360-degree continuous rotation on the horizontal axis and 90-degree movement on the vertical axis), two rows totaling 12 RGB LEDs, infrared transmitter and receiver, a three-zone touch panel, and a full-featured NFC module.
The **robot body**, connected to the main unit, includes a USB-C interface for power and data, a 550 mAh battery, two feedback servos (360-degree continuous rotation on the horizontal axis and 90-degree movement on the vertical axis), two rows totaling 12 RGB LEDs, infrared transmitter and receiver, a three-zone touch panel, and a full-featured NFC module.
The **factory firmware** comes with rich features, including vivid and cute facial expressions and motions, the XiaoZhi AI agent, as well as support for iOS app video calls, remote avatars, and discovering other nearby StackChan devices. The product also supports programming via Arduino and UiFlow2, making it easy to implement a wide range of custom functionalities.
The **factory firmware** is feature-rich, including an AI Agent, lively and expressive animations, ESP-NOW wireless remote control, and online app downloads. It can connect to a mobile app for video viewing, remote avatar control, and more, and also supports online updates (OTA). The product also supports programming via Arduino, UiFlow2, and other methods, and can connect to various expansion units in the M5Stack ecosystem, making it easy to implement a wide range of custom functions.
> Do not forcibly rotate any movable parts connected to the motors by hand when you are unsure whether the motors are powered and under control, as this may cause hardware damage.
> ⚠️ Do not forcibly rotate any movable parts connected to the motors by hand when you are unsure whether the motors are powered and under control, as this may cause hardware damage.
- **StackChan World iOS app**: https://apps.apple.com/app/stackchan-world/id6756086326
- Purchase link: [M5Stack Official Store](https://shop.m5stack.com/products/stackchan-kawaii-co-created-open-source-ai-desktop-robot) | [淘宝 Taobao](https://item.taobao.com/item.htm?id=1042238294510)
- **StackChan World website**: https://stackchan.world/home
- Product document page: [English](https://docs.m5stack.com/en/StackChan) | [日本語](https://docs.m5stack.com/ja/StackChan) | [中文](https://docs.m5stack.com/zh_CN/StackChan)
- Board support package: https://github.com/m5stack/StackChan-BSP
Thank you to the contributors of the StackChan community, especially:
| ![](https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1205/avatar_stack_chan.jpg) | ![](https://m5stack-doc.oss-cn-shenzhen.aliyuncs.com/1205/avatar_takao.jpg) |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [@stack_chan](https://x.com/stack_chan) | [@mongonta555](https://x.com/mongonta555) |
| Shinya Ishikawa | Takao Akaki |
+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"
}
}
}

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