Initial commit

This commit is contained in:
sky.min 2026-08-20 09:25:05 +08:00
commit 09b892c3d2
104 changed files with 8459 additions and 0 deletions

20
.gitignore vendored Executable file
View File

@ -0,0 +1,20 @@
# Flutter/Dart build artifacts
build/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
# SDK delivery output
dist/
# IDE
.idea/
*.iml
.vscode/
# macOS
.DS_Store
# FVM Version Cache
.fvm/
.fvmrc

380
README.md Normal file
View File

@ -0,0 +1,380 @@
# StarCloud Flutter SDK
Flutter 智能锁接入 SDK。SDK 封装了星云账户、蓝牙扫描和配对、锁管理、凭证管理、网关 WiFi 配网、锁状态/设置,以及视频对讲信令与媒体数据事件。
> 当前交付面向 Android 和 iOS;不提供 macOS、Windows、Linux 或 Web 的桌面端支持。
## 交付内容
实际交付根目录包含下列五个同级目录。`flutter_sdk` 通过相对路径依赖前三个底层包;`video_decode_plugin` 是示例工程的视频渲染可选组件:
```text
vendor/
├── flutter_sdk/ # 本 SDK
├── starlock_ble/ # 蓝牙协议与设备连接层
├── starcloud/ # 云端、设备与领域模型
├── starchart/ # 视频对讲信令与传输层
└── video_decode_plugin/ # 可选:示例使用的 H264 FFmpeg 渲染插件
```
`example/` 是可运行的集成示例,覆盖常用的接入路径。不要将示例中的测试账号、日志或 UI 直接用于生产环境。
## 环境与平台要求
| 项目 | 要求 |
| --- | --- |
| Flutter | 3.19.6 或更高版本 |
| Dart | 3.3.0 或更高版本 |
| Android | `minSdkVersion 23`,真机蓝牙调试 |
| iOS | iOS 12.0 或更高版本,真机调试 |
| 网络 | 可访问已获授权的星云服务和视频对讲服务 |
蓝牙扫描、配对、门锁控制、WiFi 配网、麦克风和相机相关能力均需要在真机验证。模拟器不具备完整的蓝牙能力。
## 安装
将 `flutter_sdk`、`starlock_ble`、`starcloud`、`starchart` 放入宿主 App 的 `vendor/`。需要使用示例同款 FFmpeg 视频渲染时,再一并放入 `video_decode_plugin`。
宿主 `pubspec.yaml` 的最小依赖只需要声明 `flutter_sdk`:
```yaml
dependencies:
flutter:
sdk: flutter
flutter_sdk:
path: vendor/flutter_sdk
```
`flutter_sdk` 的 `pubspec.yaml` 已通过 `path: ../starlock_ble`、`../starcloud`、`../starchart` 引用同级底层包,因此宿主执行一次依赖安装即可递归解析全部核心包:
```bash
fvm flutter pub get
```
如需将示例的 H264 FFmpeg 解码渲染带入宿主 App,额外声明:
```yaml
video_decode_plugin:
path: vendor/video_decode_plugin
```
然后仍只需要在宿主 App 根目录再执行一次 `fvm flutter pub get`。**不需要**为正常集成专门进入 `video_decode_plugin`、`starlock_ble`、`starcloud` 或 `starchart` 逐个执行 `pub get`;只有开发/测试这些包自身或运行它们的独立 example 时才需要。
如果项目不使用 FVM,请使用与本 SDK 兼容的 Flutter 版本执行 `flutter pub get`。
## 平台配置
### Android
确认 `android/app/build.gradle` 的最小版本不低于 23,并在 `android/app/src/main/AndroidManifest.xml` 添加按业务实际需要的权限:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
```
- 仅使用锁管理时,麦克风、相机权限可不声明。
- Android 12 及以上需要在运行时申请 `BLUETOOTH_SCAN`、`BLUETOOTH_CONNECT`。
- 若视频对讲服务使用 `http://` 地址,Android 需要允许明文流量;生产环境建议使用 HTTPS。
### iOS
在 `ios/Podfile` 中设置:
```ruby
platform :ios, '12.0'
```
在 `ios/Runner/Info.plist` 添加适合实际业务的用途说明:
```xml
<key>NSBluetoothAlwaysUsageDescription</key>
<string>用于扫描和连接智能锁。</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>用于连接智能锁设备。</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>用于发现附近的蓝牙智能锁。</string>
<key>NSMicrophoneUsageDescription</key>
<string>用于视频对讲。</string>
<key>NSCameraUsageDescription</key>
<string>用于视频对讲和扫码功能。</string>
```
完成后执行:
```bash
cd ios && pod install && cd ..
```
## 接入顺序
常规业务建议严格按以下顺序执行:
```text
bootstrap
→ auth.init
→ createCloudUser / setCloudAccounts
→ 扫描、配对或获取锁列表
→ 构建 StarCloudLockContext
→ 锁控制、凭证、WiFi 配网或视频对讲
```
`CloudUserInfo` 是扫描、配对及云端锁管理的前提;`lock.skychip.top` 的 WiFi 授权登录令牌仅用于网关配网,不能替代 SDK 云账户。
## 快速接入
### 1. 创建客户端并初始化认证
应用启动时只调用一次 `bootstrap()`:
```dart
import 'package:flutter/widgets.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
StarCloudClient.bootstrap();
runApp(const MyApp());
}
```
从星云后台注册应用并获取 `clientId`、`clientSecret`。不要把密钥提交到代码仓库;建议由业务后端下发或通过构建参数注入:
```dart
const clientId = String.fromEnvironment('STARCLOUD_CLIENT_ID');
const clientSecret = String.fromEnvironment('STARCLOUD_CLIENT_SECRET');
const environmentUrl = String.fromEnvironment('STARCLOUD_ENV_URL');
StarCloudClient.instance.auth.init(
clientId: clientId,
clientSecret: clientSecret,
environmentUrl: environmentUrl,
);
```
例如:
```bash
fvm flutter run \
--dart-define=STARCLOUD_CLIENT_ID=your_client_id \
--dart-define=STARCLOUD_CLIENT_SECRET=your_client_secret \
--dart-define=STARCLOUD_ENV_URL=https://your-authorized-host
```
### 2. 设置 SDK 云账户
生产环境应由业务后台保存、恢复并下发合法的 `CloudUserInfo`。仅在联调或示例中,可创建临时云账户:
```dart
final cloudUser = await StarCloudClient.instance.auth.createCloudUser();
StarCloudClient.instance.auth.setCloudAccounts([cloudUser]);
```
若有多个云账户,使用 `setCloudAccounts` 一次性设置,并在设备业务 API 中按需传入 `cloudUid`。
### 3. 扫描、配对与锁控制
```dart
final devices = await StarCloudClient.instance.device.scanDeviceList(
timeout: 10,
onScan: (list) {
// 可持续刷新扫描列表
},
);
if (devices.isEmpty) return;
final lock = await StarCloudClient.instance.device.pairDevice(
scanResult: devices.first,
);
await StarCloudClient.instance.device.openLock(lockId: lock.lockId);
await StarCloudClient.instance.device.closeLock(lockId: lock.lockId);
```
扫描页应直接为每个扫描结果提供“配对”操作;若本地已配对锁的 `mac` 或蓝牙设备名与扫描结果一致,应展示“已配对”而非再次配对。离开页面或用户取消时调用:
```dart
await StarCloudClient.instance.device.stopScan();
await StarCloudClient.instance.device.disconnect();
```
已配对设备可从云端获取:
```dart
final list = await StarCloudClient.instance.device.getDeviceList(
pageNo: 1,
pageSize: 20,
);
final latestLock = await StarCloudClient.instance.device.getDevice(
lockId: lock.lockId,
);
```
### 4. 使用 `StarCloudLockContext`
高级 BLE 接口(状态、功能位、网关配网)需要锁身份、蓝牙密钥和本地握手 token。请从已配对或已查询到的 `StarCloudLock` 构建上下文,不要自行拼装密钥:
```dart
final context = await StarCloudLockContext.fromLock(latestLock);
final state = await StarCloudClient.instance.lockState
.readLockStateByContext(context);
await StarCloudClient.instance.lockSetting.setRemoteUnlockByContext(
context,
enabled: true,
);
```
上下文依赖已经完成的蓝牙认证握手;token 缺失或过期时,应重新连接/配对并获取最新锁信息后重试。
### 5. 网关 WiFi 配网
网关配网需要:已配对的网关锁、SDK 云账户、锁主账号或管理员账号的 WiFi 授权令牌,以及 App 视频对讲 PeerID。
先登录 WiFi 授权服务:
```dart
final accessToken = await StarCloudClient.instance.auth.loginLockService(
username: username,
password: password,
deviceBrand: 'android',
deviceModel: 'model-name',
deviceVersion: 'os-version',
);
```
初始化视频对讲以取得 App PeerID(见下一节),随后执行:
```dart
final context = await StarCloudLockContext.fromLock(lock);
final reply = await StarCloudClient.instance.wifi.configureWifiByContext(
context,
ssid: ssid,
password: password,
lockServiceAccessToken: accessToken,
deviceName: lock.bluetooth.bluetoothDeviceName,
mac: lock.mac,
);
// 配网响应包含锁端 peerId;仍需轮询云端确认锁上线。
final status = await StarCloudClient.instance.wifi.getGatewayStatus(
lockId: lock.bluetooth.bluetoothDeviceName,
userId: context.userId,
deviceName: lock.bluetooth.bluetoothDeviceName,
mac: lock.mac,
);
```
`configureWifiByContext` 会获取网关配置、通过 BLE 下发,并向云端提交设备网络信息。BLE 返回成功只表示流程已接受,不代表锁已联网;请使用 `getDevice(lockId: ...)` 轮询 `isOnline` 与 `peerId`。
### 6. 视频对讲
视频对讲服务只负责注册、呼叫和事件分发;H264 解码渲染、G711 编解码、麦克风采集与权限处理由宿主 App 集成。示例工程使用本地 `video_decode_plugin` 的 FFmpeg 后端进行 H264 解码渲染。
```dart
await StarCloudClient.instance.talk.init(
productName: 'YourApp',
unique: deviceUniqueId,
baseUrl: 'http://scd.skychip.top:8710',
);
final appPeerId = StarCloudClient.instance.talk.myPeerId;
```
先订阅事件,再响应来电、视频协商与状态:
```dart
final incomingSub = StarCloudClient.instance.talk.onIncomingCall.listen((call) {
// 展示接听/拒绝 UI
});
final mediaSub = StarCloudClient.instance.talk.onTalkData.listen((event) {
// event.isAudio:G711 音频;否则为视频数据,交给宿主解码器
});
final expectSub = StarCloudClient.instance.talk.onTalkExpectResp.listen((event) {
// 根据 width、height、rotate 与 videoType 重建/调整渲染器
});
final statusSub = StarCloudClient.instance.talk.onTalkStatus.listen((event) {
// 根据 TalkStatus 更新 UI 与资源生命周期
});
```
主动监控、接听和挂断:
```dart
StarCloudClient.instance.talk.requestCall(
lockPeerId: lockPeerId,
callerName: 'YourApp Monitor',
options: const TalkOptions(
preferredVideoTypes: [VideoTypeE.H264],
preferredAudioTypes: [],
),
);
StarCloudClient.instance.talk.accept(lockPeerId: incomingCall.lockPeerId);
StarCloudClient.instance.talk.hangup(lockPeerId: lockPeerId);
```
停止使用时取消订阅并释放宿主侧解码器、录音和播放器;在应用彻底退出视频对讲模块时调用 `StarCloudClient.instance.talk.dispose()`。
## 服务一览
| 服务 | 入口 | 典型能力 |
| --- | --- | --- |
| 认证 | `auth` | 初始化、SDK 云账户、WiFi 授权登录 |
| 设备 | `device` | 扫描、配对、锁列表、开关锁、重置 |
| 密码 | `password` | 查询、在线/离线密码、重置 |
| 卡片/生物识别 | `card`、`biometric` | IC 卡、指纹、人脸管理 |
| 电子钥匙 | `electricKey` | 查询、新增、编辑、冻结、删除 |
| 记录 | `record` | 查询与上报锁记录 |
| 状态/设置 | `lockState`、`lockSetting` | 锁状态、功能位读取与设置 |
| WiFi | `wifi` | WiFi 搜索、网关/锁板配网、网关状态 |
| 视频对讲 | `talk` | 注册、来电、呼叫、媒体事件、挂断 |
高级 BLE 同类请求应串行发送,避免多个相同 Reply 类型的命令同时在途导致响应匹配错误。
## 示例工程
示例位于 [example](example/README.md),当前操作顺序为:
1. 登录页注册星云应用,填写 `client_id` 与 `client_secret` 并初始化 SDK;创建测试云账户或接入业务账户。
2. 在设备扫描页扫描、直接配对门锁。
3. 在 WiFi 配网页完成“第 1 步:初始化视频对讲”,然后输入 WiFi 名称/密码并执行“第 2 步:WiFi 配网”。
4. 在视频对讲页对已联网且有 `peerId` 的锁发起监控,或处理来电。
## 故障排查
| 现象 | 优先检查 |
| --- | --- |
| `auth.init` 或 `createCloudUser` 失败 | `clientId`、`clientSecret`、授权环境地址与网络连通性 |
| 扫描不到锁 | 真机蓝牙/定位已开启,运行时权限已授予,锁处于可发现状态 |
| 配对或高级 BLE 命令超时 | 设备距离、蓝牙连接、锁的认证握手及 `StarCloudLockContext` token |
| WiFi 配网成功但无法监控 | 配网后等待锁上线,轮询锁详情确认 `isOnline == true` 且 `peerId` 非空 |
| 视频无画面 | 确认对讲已初始化、锁端协商到 H264、宿主解码器已处理 SPS/PPS/关键帧与旋转比例 |
| iOS 编译失败 | Podfile 最低版本、权限声明和 `pod install` 是否完成 |
提交支持请求时,请提供 Flutter 版本、设备型号/系统版本、操作时间、脱敏后的完整日志、锁型号和最小复现步骤。请不要提交 `clientSecret`、访问令牌、私钥或完整用户信息。
## 安全与发布
- `clientSecret`、WiFi 授权令牌、蓝牙密钥和视频对讲凭据均为敏感数据,不得写入日志或提交版本库。
- 生产环境应由业务后端管理应用凭据与用户身份,不建议在客户端内置长期密钥。
- 上线前请使用正式包名、签名、隐私政策和实际权限用途说明;按应用商店要求审核蓝牙、定位、相机与麦克风权限。
- SDK 功能与门锁固件、账号权限和已开通服务相关;请在量产前完成真实锁、弱网和异常断连测试。
## 支持
联系技术支持时请附上项目名称、SDK 版本、目标平台、问题模块和上述脱敏诊断信息。

43
example/.gitignore vendored Executable file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
example/.metadata Executable file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: android
create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

119
example/README.md Normal file
View File

@ -0,0 +1,119 @@
# StarCloud Flutter SDK 示例
本示例用于验证 `flutter_sdk` 的移动端集成流程,包含 SDK 认证、测试云账户、蓝牙扫描/配对、基础锁控制、网关 WiFi 配网,以及视频对讲监控与来电处理。
> 示例用于联调和能力演示。生产项目必须自行实现账号体系、密钥保护、错误提示、权限说明与日志脱敏。
## 环境
- Flutter 3.19.6(项目通过 FVM 管理)
- Android 真机或 iOS 真机
- 已按根目录 [README](../README.md) 配置蓝牙、定位、网络、麦克风和相机权限
- 需要 `flutter_sdk`、`starlock_ble`、`starcloud`、`starchart` 及 `video_decode_plugin` 位于当前相对路径所指向的位置
安装依赖并运行:
```bash
fvm use 3.19.6
fvm flutter pub get
fvm flutter run
```
不需要先进入 `../../video_decode_plugin` 执行 `fvm flutter pub get`:本示例的 `pubspec.yaml` 已以 `path: ../../video_decode_plugin` 声明该插件,执行本目录的 `fvm flutter pub get` 会递归解析其依赖。只有修改插件源码或运行插件自身示例时,才进入插件目录单独执行该命令。
若只做代码检查:
```bash
fvm dart analyze
```
## 使用流程
### 1. 登录
在“登录”页填写在星云平台注册后获得的 `client_id` 与 `client_secret`,点击:
```text
第1步:注册星云账号得到client_id和client_secret初始化SDK
```
环境地址已写在示例代码中:
```text
https://cloud.skychip.top/sdk
```
随后点击“第2步:仅测试创建新的 SDK 星云账户”。生产环境请替换为业务后台下发的 `CloudUserInfo`,不要将测试账户创建逻辑直接发布。
如需网关配网,再输入锁主账号/管理员账号并点击:
```text
第3步:填写app登录账号支持WIFI授权账号
```
该账号的授权令牌仅用于 WiFi 网关配网,不能代替 SDK 云账户。
### 2. 设备扫描与配对
进入“设备扫描”页,点击“扫描”。每条扫描结果会直接显示:
- `配对`:可对该设备发起配对;
- `已配对`:当前设备已匹配本地配对锁,不会重复配对。
配对成功后可在“蓝牙控锁”页验证基础开锁、关锁能力。
### 3. WiFi 配网
进入“WiFi配网”页,按顺序操作:
1. 点击“第1步:初始化视频对讲”,生成 App PeerID;
2. 输入 WiFi SSID、密码;
3. 点击“第2步:wifi配网”。
配网下发成功后,锁仍可能需要 30–60 秒联网。示例会查询网关状态并轮询云端锁信息;在锁的 `isOnline` 为 `true` 且 `peerId` 非空之前,不能保证监控可用。
### 4. 视频对讲
“视频对讲”页不再提供初始化入口;请先在“WiFi配网”页完成第 1 步。该页提供:
- 锁 PeerID 自动回填与手动输入;
- `监控`、`挂断`;
- 来电接听/拒绝卡片;
- H264 视频 Texture 渲染、状态与诊断日志。
示例通过 `video_decode_plugin` 选择 FFmpeg 解码后端。启动监控时日志应包含:
```text
[DIAG] recreateDecoder preferred=ffmpeg selected=ffmpeg
[DIAG] decoder active engine=ffmpeg
```
若日志显示 `mediacodec`,说明运行的不是当前 FFmpeg 配置;请检查本地 `video_decode_plugin` 路径、重新构建 APK,并确认未使用旧产物。
## 示例配置位置
| 配置 | 文件 | 说明 |
| --- | --- | --- |
| 星云环境地址 | `lib/tabs/auth_tab.dart` | 示例固定为测试环境地址;按已授权环境修改 |
| 视频对讲服务地址 | `lib/services/video_talk_initializer.dart` | 示例固定地址;生产按服务分配修改 |
| WiFi SSID/密码 | 页面输入 | 不设默认值,也不会保存 |
| FFmpeg 视频渲染 | `lib/tabs/video_talk_tab.dart` | 初始化前后校验活动引擎为 `ffmpeg` |
## 重要说明
- 请不要向代码仓库提交真实 `client_secret`、账号密码、访问令牌、蓝牙密钥或视频凭据。
- Android 模拟器不适合测试蓝牙扫描、配对和 WiFi 配网;请使用真机。
- 监控超时通常由锁未联网、锁端 `peerId` 未上报、网关未完成配网或锁端未回复对讲协商造成。
- 示例目前不包含生产级账号持久化、异常恢复、埋点或 UI 设计,应按业务要求实现。
## 目录
```text
lib/
├── tabs/ # 登录、扫描、锁控、WiFi、视频对讲页面
├── services/
│ ├── cat_eye_monitor_service.dart
│ └── video_talk_initializer.dart
├── global.dart # 跨页面的锁、PeerID、授权状态
└── main.dart # 页面导航与 SDK bootstrap
```

28
example/analysis_options.yaml Executable file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
example/android/.gitignore vendored Executable file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,62 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.example"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "com.example.example"
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,56 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application
android:label="example_demo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:usesCleartextTraffic="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embed2ding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

21
example/android/build.gradle Executable file
View File

@ -0,0 +1,21 @@
allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/public' }
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,8 @@
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC
# AndroidX 迷离
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

26
example/android/settings.gradle Executable file
View File

@ -0,0 +1,26 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}
include ":app"

34
example/ios/.gitignore vendored Executable file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,24 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
example/ios/Podfile Executable file
View File

@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -0,0 +1,623 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
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 = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = NAQ5PL2DYC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
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 = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = 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_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_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
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 = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = NAQ5PL2DYC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = NAQ5PL2DYC;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterSdkExampleTemp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,13 @@
# iOS 启动页资源
此目录用于存放 iOS 启动页的图片资源。当前 Flutter 项目使用 `LaunchScreen.storyboard`;如需自定义品牌启动页,请优先在 Xcode 中维护资源目录,而不是在 Dart 页面中延迟加载图片。
## 修改方式
1. 在 macOS 上执行 `open ios/Runner.xcworkspace`;
2. 在 Xcode 的 Project Navigator 中选择 `Runner/Assets.xcassets`;
3. 选择 `LaunchImage` 或新建对应 Image Set,并按 `Contents.json` 中要求填入各尺寸图片;
4. 确认 `ios/Runner/Info.plist` 的 `UILaunchStoryboardName` 仍为 `LaunchScreen`;
5. 使用真机和模拟器分别验证暗色模式、不同屏幕尺寸与横竖屏显示。
不要在启动页展示账号密码、设备信息、动态网络内容或需要异步加载的图片。启动页应保持静态、简洁,并符合 Apple 审核规范。

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

78
example/ios/Runner/Info.plist Executable file
View File

@ -0,0 +1,78 @@
<?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>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>该应用需要蓝牙权限以连接智能锁设备</string>
<key>NSBluetoothCentralUsageDescription</key>
<string>该应用需要蓝牙权限来扫描和连接智能锁</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>该应用需要位置权限以扫描周围蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>该应用需要麦克风权限</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>example_demo</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>example_demo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

26
example/lib/global.dart Executable file
View File

@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl;
// 全局共享状态
final ValueNotifier<int?> pairedLockId = ValueNotifier<int?>(null);
final ValueNotifier<scl.StarCloudLock?> pairedLock =
ValueNotifier<scl.StarCloudLock?>(null);
// 关键:App 的星图 PeerID(视频对讲初始化后产生,用于配网时告知锁端)
final ValueNotifier<String?> appPeerId = ValueNotifier<String?>(null);
// 视频对讲服务由 WiFi 配网页的第 1 步初始化,供配网和视频对讲页共用。
final ValueNotifier<bool> videoTalkReady = ValueNotifier<bool>(false);
final ValueNotifier<bool> videoTalkInitializing = ValueNotifier<bool>(false);
// lock.skychip.top 授权登录返回的动态 token,仅用于 WiFi 网关配网。
// 它与 cloud.skychip.top 的 OAuth token 属于不同认证域。
final ValueNotifier<String?> lockServiceAccessToken =
ValueNotifier<String?>(null);
// SDK 云账户是扫描、绑定和锁密钥操作的前置条件。生产环境应由业务后台
// 按用户保存并下发 CloudUserInfo;Demo 可设置后台账户或创建测试账户。
final ValueNotifier<bool> sdkCloudAccountReady = ValueNotifier<bool>(false);
// 当前完成 lock.skychip.top 授权的账号,仅供 WiFi 网关业务使用。
final ValueNotifier<String?> signedInAccount = ValueNotifier<String?>(null);

125
example/lib/l10n.dart Executable file
View File

@ -0,0 +1,125 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class S {
static const _languagePreferenceKey = 'is_english';
static final ValueNotifier<bool> langNotifier = ValueNotifier<bool>(false);
static bool get isEn => langNotifier.value;
static set isEn(bool value) => langNotifier.value = value;
static Future<void> loadLanguagePreference() async {
try {
final preferences = await SharedPreferences.getInstance();
isEn = preferences.getBool(_languagePreferenceKey) ?? false;
} catch (error) {
debugPrint('Failed to load language preference: $error');
isEn = false;
}
}
static Future<void> setLanguagePreference(bool value) async {
isEn = value;
try {
final preferences = await SharedPreferences.getInstance();
await preferences.setBool(_languagePreferenceKey, value);
} catch (error) {
debugPrint('Failed to save language preference: $error');
}
}
// App Title
static String get title => isEn ? 'Demo' : '示例';
// Tabs
static String get tabInit => isEn ? 'Login' : '登录';
static String get tabScan => isEn ? 'Scan' : '设备扫描';
static String get tabLock => isEn ? 'Lock' : '蓝牙控锁';
static String get tabWifi => isEn ? 'WiFi' : 'WiFi配网';
static String get tabTalk => isEn ? 'Talk' : '视频对讲';
// Common Logs & Buttons
static String get logFailed => isEn ? 'Failed' : '失败';
static String get logSuccess => isEn ? 'Success' : '成功';
static String get btnProcessing => isEn ? 'Processing...' : '处理中...';
// Auth Tab
static String get sdkStatus => isEn ? 'SDK Status' : 'SDK 状态';
static String get cloudUserStatus => isEn ? 'Signed-in Account' : '登录账户';
static String get clientId => isEn ? 'Client ID *' : '客户ID *';
static String get clientSecret => isEn ? 'Client Secret' : '客户端密钥';
static String get envUrl => isEn ? 'Env URL' : '环境地址';
static String get step1Init => isEn
? 'Step 1: Register a cloud account to get client_id and client_secret, then initialize the SDK'
: '第1步:注册星云账号填写后初始化SDK';
static String get step2User => isEn ? 'Step 2: Sign In' : '第 2 步:登录账号';
static String get btnCreateTestCloudAccount => isEn
? 'Step 2: Create a new SDK cloud account (testing only)'
: '第2步:仅测试创建新的 SDK 星云账户';
static String get btnLoginForWifiAuthorization => isEn
? 'Step 3: Sign in with the app account for WiFi authorization'
: '第3步:填写 app 登录账号,支持 WIFI 授权账号';
static String get accountLabel => isEn ? 'Account' : '账号';
static String get passwordLabel => isEn ? 'Password' : '密码';
static String get logFillId =>
isEn ? 'Please fill Client ID' : '请先填写 Client ID';
static String get logInitSuccess => isEn ? 'SDK Initialized' : 'SDK 初始化成功';
static String get logUserReady => isEn ? 'Account ready' : '账号登录成功';
// Scan Tab
static String get btnScan => isEn ? 'Scan' : '扫描';
static String get btnScanning => isEn ? 'Scanning...' : '扫描中...';
static String get btnStop => isEn ? 'Stop' : '停止';
static String get scanHint => isEn ? 'Tap "Scan" to start' : '点击"扫描设备"开始';
static String get pairedStatus => isEn ? 'Paired' : '已配对';
static String get btnPair => isEn ? 'Pair' : '配对';
static String get logScanning =>
isEn ? 'Scanning (10s)...' : '开始扫描(10 秒超时)...';
static String get logScanDone => isEn ? 'Scan done' : '扫描完成';
// Lock Tab
static String get lockIdLabel => isEn ? 'Lock ID' : '锁定ID';
static String get notPaired => isEn ? 'Not Paired' : '未配对';
static String get btnOpen => isEn ? 'Open' : '开锁';
static String get btnClose => isEn ? 'Close' : '关锁';
// WiFi Tab
static String get wifiTitle => isEn ? 'WiFi Config' : 'WiFi配网';
static String get wifiModeDirect =>
isEn ? 'Direct Mode (Lock Board)' : '锁板直连模式';
static String get wifiModeGateway => isEn ? 'Gateway Mode' : '网关模式';
static String get wifiConfigSec => isEn ? 'WiFi Settings' : 'WiFi 配置';
static String get btnStartConfig =>
isEn ? 'Step 2: WiFi Setup' : '第2步:wifi配网';
static String get logConfigSent => isEn ? 'Config Sent' : '指令已发送';
static String get currentLoggedInAccountPrefix =>
isEn ? 'Current signed-in account: ' : '当前登录账号:';
static String get wifiUsesAuthorizedSession => isEn
? 'WiFi setup will use this account\'s authorized session.'
: 'WIFI 配网将使用此账号的授权会话。';
static String get logConfigCancelledNeedTalkInitialization => isEn
? 'WiFi setup cancelled: Please tap “Step 1: Initialize Video Talk” on this page first.'
: '已取消配网:请先点击本页“第1步:初始化视频对讲”。';
static String get logUsingLoginSessionForGatewayConfig => isEn
? 'Using the signed-in session to get the gateway configuration and send it via BLE...'
: '已使用登录会话,正在获取网关配置并通过 BLE 下发...';
static String get logWifiConfigCompleted => isEn
? 'SUCCESS: BLE WiFi provisioning and deviceNetwork/setting are complete.'
: 'SUCCESS: BLE 配网及 deviceNetwork/setting 均已完成。';
// Talk Tab
static String get talkServer => isEn ? 'Server Address' : '服务器地址';
static String get btnInitTalk =>
isEn ? 'Step 1: Initialize Video Talk' : '第1步:初始化视频对讲';
static String get initialized => isEn ? 'Initialized' : '已初始化';
static String get completeWifiTalkInitialization => isEn
? 'Please complete Step 1: Initialize Video Talk on the WiFi Setup tab.'
: '请先在“WiFi配网”页完成第1步:初始化视频对讲。';
static String get incomingCall => isEn ? 'Incoming Call' : '来电';
static String get btnAccept => isEn ? 'Accept' : '接听';
static String get btnReject => isEn ? 'Reject' : '拒绝';
static String get btnHangup => isEn ? 'Hang up' : '挂断';
static String get btnMonitor => isEn ? 'Monitor' : '监控';
static String get logTalkReady => isEn ? 'Talk Ready' : '视频对讲就绪';
static String get deviceIdLabel => isEn ? 'Device ID' : '设备ID';
}

107
example/lib/main.dart Executable file
View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import 'l10n.dart';
import 'tabs/auth_tab.dart';
import 'tabs/device_scan_tab.dart';
import 'tabs/lock_control_tab.dart';
import 'tabs/video_talk_tab.dart';
import 'tabs/wifi_config_tab.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await S.loadLanguagePreference();
StarCloudClient.bootstrap();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<void> _toggleLanguage() async {
// 切换 ValueNotifier 的值,这会触发下方的 ValueListenableBuilder
await S.setLanguagePreference(!S.isEn);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: S.langNotifier,
builder: (context, isEn, _) {
return MaterialApp(
// 关键:使用 ValueKey 强制整个 App 在语言切换时重新构建,确保所有 S.xxx 文字刷新
key: ValueKey(isEn),
title: S.title,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme:
ColorScheme.fromSeed(seedColor: const Color(0xFF0066CC)),
useMaterial3: true,
),
home: MainScreen(onToggleLang: _toggleLanguage),
);
},
);
}
}
class MainScreen extends StatefulWidget {
final VoidCallback onToggleLang;
const MainScreen({super.key, required this.onToggleLang});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final _tabs = [
const AuthTab(),
const DeviceScanTab(),
const LockControlTab(),
const WifiConfigTab(),
const VideoTalkTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.title),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
TextButton.icon(
onPressed: widget.onToggleLang,
icon: const Icon(Icons.language),
label: Text(S.isEn ? '中文' : 'EN'),
)
],
),
body: IndexedStack(
index: _currentIndex,
children: _tabs,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) => setState(() => _currentIndex = index),
destinations: [
NavigationDestination(
icon: const Icon(Icons.settings), label: S.tabInit),
NavigationDestination(
icon: const Icon(Icons.bluetooth_searching), label: S.tabScan),
NavigationDestination(
icon: const Icon(Icons.lock_outline), label: S.tabLock),
NavigationDestination(icon: const Icon(Icons.wifi), label: S.tabWifi),
NavigationDestination(
icon: const Icon(Icons.videocam), label: S.tabTalk),
],
),
);
}
}

View File

@ -0,0 +1,151 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart' as crypto;
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import 'package:starcloud/blue/blue_features.dart';
import 'package:starcloud/blue/blue_read_write.dart';
import 'package:starcloud/blue/entity/blue_message.dart';
import 'package:starcloud/device/ble/base/base_sender.dart';
import 'package:starcloud/device/ble/ble_reply.dart';
import 'package:starcloud/device/ble/business/ble_type.dart';
import 'package:starcloud/device/ble/tool/ble_tool.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart';
import 'package:starcloud/tool/sm4_encipher/sm4.dart';
/// 主动监控前将猫眼从省电模式切换到实时监控模式。
///
/// 实时模式由 BLE feature bit 64 写入锁端。
/// 省电模式(catEyeMode=0)下锁固件会忽略主动 TalkRequest。
///
/// 公开 Flutter SDK 文档未定义 cat-eye 云端设置 API;因此这里不能调用
/// 非公开的 `updateCatEyeConfig` 并把其服务端权限错误当作 BLE 操作失败。
class CatEyeMonitorService {
static const int _catEyeFeatureBit = 64;
static const int _realtimeMode = 2;
Future<void> enableRealtimeMode({
required StarCloudLock lock,
}) async {
final context = await StarCloudLockContext.fromLock(lock);
final featureData = <int>[
_realtimeMode,
0, 0, 0, 0, 0, 0, // 非自定义模式的录像参数
0, 0, // detectionDistance / realTimePicture
1, 10, 1, 1, // 自动亮屏、亮屏时长、逗留/异常告警
];
final blue = BlueReadWrite.shareManager();
if (blue == null) {
throw const StarCloudException('Bluetooth service is unavailable');
}
blue.deviceName = lock.bluetooth.bluetoothDeviceName;
if (!Platform.isAndroid) blue.mac = lock.mac;
blue.type = BlueFeaturesType.lock;
blue.clearCallback();
final reply = await blue.writeAndReturn(
_SetCatEyeModeCommand(
keyId: context.keyId,
userId: context.userId,
featureData: featureData,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
),
getPrivateKey: () => context.privateKey,
timeout: const Duration(seconds: 20),
);
if (reply.status != 0) {
throw StarCloudException(
'Failed to enable cat-eye realtime mode over BLE',
raw: 'status=0x${reply.status.toRadixString(16)}',
);
}
}
}
class _SetCatEyeModeCommand extends SenderProtocol {
_SetCatEyeModeCommand({
required this.keyId,
required this.userId,
required this.featureData,
required List<int> token,
required this.needAuthor,
required this.publicKey,
required this.privateKey,
}) : token = List<int>.from(token),
super(CommandType.generalExtendedCommond);
final String keyId;
final String userId;
final List<int> featureData;
final int needAuthor;
final List<int> publicKey;
final List<int> privateKey;
@override
List<int>? token;
@override
List<int> messageDetail() {
final data = <int>[0x30, 0x30, 72];
var subData = <int>[];
final keyBytes = utf8.encode(keyId);
subData.addAll(keyBytes);
subData = BLETool.getFixedLengthList(subData, 40 - keyBytes.length);
final userBytes = utf8.encode(userId);
subData.addAll(userBytes);
subData = BLETool.getFixedLengthList(subData, 20 - userBytes.length);
subData
..add(CatEyeMonitorService._catEyeFeatureBit)
..add(featureData.length)
..addAll(featureData);
if (needAuthor == 0) {
subData.add(0);
} else {
final authData = <int>[
...userBytes,
...keyBytes,
...?token,
...publicKey,
];
final authCode = crypto.md5.convert(authData).bytes;
subData
..add(authCode.length)
..addAll(authCode);
}
data
..add(subData.length)
..addAll(subData);
while (data.length % 16 != 0) {
data.add(0);
}
return SM4.encrypt(data, key: privateKey, mode: SM4CryptoMode.ECB);
}
@override
BLEReply blueMessageToReply(BlueMessage message) {
return _SetCatEyeModeReply.parseData(commandType, message.data);
}
}
class _SetCatEyeModeReply extends BLEReply {
_SetCatEyeModeReply.parseData(
CommandType commandType,
List<int> data,
) : super.parseData(commandType, data) {
// 通用扩展应答:[0x30, 0x30, mainStatus, subCommand, subStatus, ...]
// 原 App 以 mainStatus 为本次设置结果。
status = data.length > 2 ? data[2] : 0xff;
errorWithStatus(status);
if (status == 0x06 && data.length >= 9) {
token = data.sublist(5, 9);
}
}
}

View File

@ -0,0 +1,46 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import '../global.dart';
const _talkServerUrl = 'http://scd.skychip.top:8710';
Future<void>? _initialization;
/// 初始化后生成的 App PeerID 会同时供 WiFi 配网及监控使用。
Future<void> initializeVideoTalk() {
if (videoTalkReady.value) return Future<void>.value();
final pending = _initialization;
if (pending != null) return pending;
final future = _initialize();
_initialization = future;
future.whenComplete(() {
if (identical(_initialization, future)) _initialization = null;
});
return future;
}
Future<void> _initialize() async {
videoTalkInitializing.value = true;
try {
var deviceId = 'dev_user_123';
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
deviceId = (await deviceInfo.androidInfo).id;
} else if (Platform.isIOS) {
deviceId = (await deviceInfo.iosInfo).identifierForVendor ?? deviceId;
}
await StarCloudClient.instance.talk.init(
productName: 'Demo',
unique: deviceId,
baseUrl: _talkServerUrl,
);
appPeerId.value = StarCloudClient.instance.talk.myPeerId;
videoTalkReady.value = true;
} finally {
videoTalkInitializing.value = false;
}
}

233
example/lib/tabs/auth_tab.dart Executable file
View File

@ -0,0 +1,233 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import '../global.dart';
import '../l10n.dart';
import '../widgets/common_widgets.dart';
class AuthTab extends StatefulWidget {
const AuthTab({super.key});
@override
State<AuthTab> createState() => _AuthTabState();
}
class _AuthTabState extends State<AuthTab> {
final _clientIdCtrl = TextEditingController();
final _clientSecretCtrl = TextEditingController();
static const _environmentUrl = 'https://cloud.skychip.top/sdk';
final _usernameCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
final List<String> _log = [];
bool _sdkReady = false;
bool _cloudUserReady = false;
bool _loggingIn = false;
void _log_(String msg) {
final ts = DateTime.now().toIso8601String().substring(11, 19);
setState(() => _log.add('[$ts] $msg'));
}
void _initSdk() {
final id = _clientIdCtrl.text.trim();
if (id.isEmpty) {
_log_(S.isEn ? 'Please fill Client ID' : '请先填写 Client ID');
return;
}
try {
StarCloudClient.instance.auth.init(
clientId: id,
clientSecret: _clientSecretCtrl.text.trim(),
environmentUrl: _environmentUrl,
);
setState(() => _sdkReady = true);
_log_(S.isEn ? 'SDK Init Success' : 'SDK 初始化成功');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
}
}
Future<void> _createDemoCloudAccount() async {
setState(() => _loggingIn = true);
_log_(S.isEn ? 'Creating SDK cloud account...' : '正在创建 SDK 星云测试账户...');
try {
final cloudUser = await StarCloudClient.instance.auth.createCloudUser();
StarCloudClient.instance.auth.setCloudAccounts([cloudUser]);
sdkCloudAccountReady.value = true;
pairedLockId.value = null;
pairedLock.value = null;
setState(() => _cloudUserReady = true);
_log_(S.isEn
? 'SDK cloud account is ready. You can scan and pair.'
: 'SDK 星云账户已就绪。现在可扫描并绑定锁。');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
} finally {
if (mounted) setState(() => _loggingIn = false);
}
}
Future<void> _loginLockService() async {
final username = _usernameCtrl.text.trim();
final password = _passwordCtrl.text;
if (username.isEmpty || password.isEmpty) {
_log_(S.isEn ? 'Please fill username and password' : '请填写用户名和密码');
return;
}
setState(() => _loggingIn = true);
_log_(S.isEn ? 'Signing in...' : '正在登录...');
try {
String deviceBrand = Platform.isAndroid ? 'android' : 'ios';
String deviceModel = 'unknown';
String deviceVersion = 'unknown';
try {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
final android = await deviceInfo.androidInfo;
deviceBrand = android.brand;
deviceModel = android.model;
deviceVersion = android.version.release;
} else if (Platform.isIOS) {
final ios = await deviceInfo.iosInfo;
deviceBrand = 'Apple';
deviceModel = ios.utsname.machine;
deviceVersion = ios.systemVersion;
}
} catch (_) {
// 设备信息只用于服务端风控,无法读取时使用默认值继续登录。
}
// lock.skychip.top token 只用于 WiFi 网关配置,不能替代 SDK 云账户。
final lockServiceToken =
await StarCloudClient.instance.auth.loginLockService(
username: username,
password: password,
deviceBrand: deviceBrand,
deviceModel: deviceModel,
deviceVersion: deviceVersion,
);
lockServiceAccessToken.value = lockServiceToken;
signedInAccount.value = username;
_log_(S.isEn ? 'WiFi authorization is ready.' : 'WiFi 网关授权登录成功。');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
} finally {
if (mounted) setState(() => _loggingIn = false);
}
}
@override
void dispose() {
_clientIdCtrl.dispose();
_clientSecretCtrl.dispose();
_usernameCtrl.dispose();
_passwordCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: S.langNotifier,
builder: (context, isEn, _) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Wrap(
spacing: 8,
children: [
StatusChip(label: S.sdkStatus, ready: _sdkReady),
StatusChip(
label: S.cloudUserStatus, ready: _cloudUserReady),
],
),
),
),
const SizedBox(height: 16),
TextField(
controller: _clientIdCtrl,
decoration: InputDecoration(
labelText: S.clientId,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.badge_outlined)),
),
const SizedBox(height: 12),
TextField(
controller: _clientSecretCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: S.clientSecret,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined)),
),
const SizedBox(height: 12),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initSdk,
icon: const Icon(Icons.settings),
label: Text(S.step1Init)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed:
_sdkReady && !_loggingIn ? _createDemoCloudAccount : null,
icon: const Icon(Icons.person_add_outlined),
label: Text(
_loggingIn ? S.btnProcessing : S.btnCreateTestCloudAccount,
),
),
const SizedBox(height: 16),
TextField(
controller: _usernameCtrl,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: S.accountLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline),
),
),
const SizedBox(height: 12),
TextField(
controller: _passwordCtrl,
obscureText: true,
onSubmitted: (_) =>
_sdkReady && !_loggingIn ? _loginLockService() : null,
decoration: InputDecoration(
labelText: S.passwordLabel,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.password),
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed:
_sdkReady && !_loggingIn ? _loginLockService : null,
icon: _loggingIn
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.login),
label: Text(
_loggingIn
? S.btnProcessing
: S.btnLoginForWifiAuthorization,
)),
const SizedBox(height: 16),
LogArea(log: _log),
],
),
);
},
);
}
}

View File

@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl;
import '../l10n.dart';
import '../global.dart';
import '../widgets/common_widgets.dart';
class DeviceScanTab extends StatefulWidget {
const DeviceScanTab({super.key});
@override
State<DeviceScanTab> createState() => _DeviceScanTabState();
}
class _DeviceScanTabState extends State<DeviceScanTab> {
bool _scanning = false;
bool _pairing = false;
List<dynamic> _devices = [];
final List<String> _log = [];
void _log_(String msg) {
final ts = DateTime.now().toIso8601String().substring(11, 19);
setState(() => _log.add('[$ts] $msg'));
}
Future<void> _scan() async {
if (!sdkCloudAccountReady.value) {
_log_(S.isEn
? 'Set an SDK cloud account before scanning and pairing a lock.'
: '请先在“登录”页创建或设置 SDK 星云账户,再扫描绑定锁。');
return;
}
setState(() {
_scanning = true;
_devices = [];
});
_log_(S.isEn ? 'Scan start (10s)...' : '开始扫描(10 秒超时)...');
try {
await StarCloudClient.instance.device.scanDeviceList(
timeout: 10,
onScan: (results) {
setState(() => _devices = results);
});
_log_(S.isEn
? 'Done. Found ${_devices.length}'
: '扫描完成,发现 ${_devices.length} 个设备');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
} finally {
setState(() => _scanning = false);
}
}
Future<void> _pair(int idx) async {
if (!sdkCloudAccountReady.value) {
_log_(
S.isEn ? 'Please set an SDK cloud account first.' : '请先设置 SDK 星云账户。');
return;
}
setState(() => _pairing = true);
_log_(S.isEn ? 'Pairing...' : '正在配对...');
try {
final lock = await StarCloudClient.instance.device
.pairDevice(scanResult: _devices[idx]);
// 更新 ValueNotifier 的值
pairedLockId.value = lock.lockId;
pairedLock.value = lock;
_log_('${S.isEn ? "Success" : "成功"}! ID: ${lock.lockId}');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
} finally {
setState(() => _pairing = false);
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: S.langNotifier,
builder: (context, isEn, _) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _scanning ? null : _scan,
icon: _scanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.bluetooth_searching),
label: Text(_scanning ? S.btnScanning : S.btnScan),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _scanning
? () => StarCloudClient.instance.device
.stopScan()
.then((_) => setState(() => _scanning = false))
: null,
child: Text(S.btnStop)),
],
),
),
// 使用 ValueListenableBuilder 监听配对状态
ValueListenableBuilder<int?>(
valueListenable: pairedLockId,
builder: (context, id, _) {
if (id == null) return const SizedBox.shrink();
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
alignment: Alignment.centerLeft,
child: Chip(
label: Text('${S.pairedStatus}: #$id'),
backgroundColor: Colors.green.shade50,
onDeleted: () {
pairedLockId.value = null;
pairedLock.value = null;
},
),
);
},
),
Expanded(
child: ValueListenableBuilder<scl.StarCloudLock?>(
valueListenable: pairedLock,
builder: (context, paired, _) {
if (_devices.isEmpty) {
return Center(
child: Text(
S.scanHint,
style: const TextStyle(color: Colors.grey),
),
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: _devices.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final device = _devices[i] as StarCloudScanResult;
final isPaired = _isPairedDevice(device, paired);
return ListTile(
title: Text(device.advName.isNotEmpty
? device.advName
: (S.isEn ? 'Unknown' : '未知设备')),
subtitle: Text('MAC: ${device.id}'),
trailing: ElevatedButton(
onPressed:
_pairing || isPaired ? null : () => _pair(i),
child: Text(isPaired ? S.pairedStatus : S.btnPair),
),
);
},
);
},
),
),
LogArea(log: _log),
],
);
},
);
}
bool _isPairedDevice(
StarCloudScanResult device,
scl.StarCloudLock? paired,
) {
if (paired == null) return false;
return paired.mac == device.id ||
paired.bluetooth.bluetoothDeviceName == device.advName;
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import '../l10n.dart';
import '../global.dart';
import '../widgets/common_widgets.dart';
class LockControlTab extends StatefulWidget {
const LockControlTab({super.key});
@override
State<LockControlTab> createState() => _LockControlTabState();
}
class _LockControlTabState extends State<LockControlTab> {
final List<String> _log = [];
bool _loading = false;
void _log_(String msg) {
final ts = DateTime.now().toIso8601String().substring(11, 19);
setState(() => _log.add('[$ts] $msg'));
}
Future<void> _run(String desc, Future<void> Function() fn) async {
if (pairedLockId.value == null) return;
_log_('$desc...');
setState(() => _loading = true);
try {
await fn();
_log_('${S.isEn ? "Success" : "成功"}');
} catch (e) {
_log_('${S.isEn ? "Failed" : "失败"}: $e');
} finally {
setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
// 监听语言切换通知
return ValueListenableBuilder<bool>(
valueListenable: S.langNotifier,
builder: (context, isEn, _) {
return Padding(
padding: const EdgeInsets.all(16),
child: ValueListenableBuilder<int?>(
valueListenable: pairedLockId,
builder: (context, id, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('${S.lockIdLabel}: ${id ?? S.notPaired}',
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ActionButton(
label: S.btnOpen,
icon: Icons.lock_open,
color: Colors.green,
loading: _loading || id == null,
onTap: () => _run(S.btnOpen,
() => StarCloudClient.instance.device.openLock(lockId: id!)),
),
),
const SizedBox(width: 8),
Expanded(
child: ActionButton(
label: S.btnClose,
icon: Icons.lock,
color: Colors.orange,
loading: _loading || id == null,
onTap: () => _run(S.btnClose,
() => StarCloudClient.instance.device.closeLock(lockId: id!)),
),
),
],
),
const SizedBox(height: 16),
LogArea(log: _log),
],
);
},
),
);
},
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,279 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_sdk/starcloud_flutter_sdk.dart';
import '../l10n.dart';
import '../global.dart';
import '../services/video_talk_initializer.dart';
import '../widgets/common_widgets.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart' as scl;
class WifiConfigTab extends StatefulWidget {
const WifiConfigTab({super.key});
@override
State<WifiConfigTab> createState() => _WifiConfigTabState();
}
class _WifiConfigTabState extends State<WifiConfigTab> {
final List<String> _log = [];
final _ssidCtrl = TextEditingController();
final _pwdCtrl = TextEditingController();
bool _loading = false;
@override
void dispose() {
_ssidCtrl.dispose();
_pwdCtrl.dispose();
super.dispose();
}
void _log_(String msg) {
final ts = DateTime.now().toIso8601String().substring(11, 19);
if (mounted) setState(() => _log.add('[$ts] $msg'));
debugPrint('WiFi_DEBUG: $msg');
}
Future<void> _initVideoTalk() async {
try {
_log_(S.isEn ? 'Initializing video talk...' : '正在初始化视频对讲...');
await initializeVideoTalk();
_log_(S.isEn
? 'Video talk is ready. App PeerID: ${appPeerId.value}'
: '视频对讲已初始化。App PeerID: ${appPeerId.value}');
} catch (e) {
_log_('${S.logFailed}: $e');
}
}
Future<void> _config() async {
final lock = pairedLock.value;
if (lock == null) {
_log_('ERROR: No device paired.');
return;
}
setState(() => _loading = true);
final String lockServiceToken = lockServiceAccessToken.value?.trim() ?? '';
if (signedInAccount.value == null || lockServiceToken.isEmpty) {
_log_('请先在“登录”页使用锁主账号/管理员账号登录。');
setState(() => _loading = false);
return;
}
_log_('>>> START CONFIG | HTTP + BLE 0x30F4/0x30F5');
try {
final ctx = await StarCloudLockContext.fromLock(lock);
final String myId = appPeerId.value ?? '';
if (myId.isEmpty) {
_log_(S.logConfigCancelledNeedTalkInitialization);
return;
}
_log_(S.logUsingLoginSessionForGatewayConfig);
final reply = await StarCloudClient.instance.wifi.configureWifiByContext(
ctx,
ssid: _ssidCtrl.text.trim(),
password: _pwdCtrl.text.trim(),
lockServiceAccessToken: lockServiceToken,
deviceName: lock.bluetooth.bluetoothDeviceName,
mac: lock.mac,
);
_log_(S.logWifiConfigCompleted);
try {
final gatewayStatus =
await StarCloudClient.instance.wifi.getGatewayStatus(
lockId: lock.bluetooth.bluetoothDeviceName,
userId: ctx.userId,
deviceName: lock.bluetooth.bluetoothDeviceName,
mac: lock.mac,
);
_log_(
'GATEWAY STATUS: 0x${gatewayStatus.status.toRadixString(16).toUpperCase()}');
} catch (e) {
_log_('GATEWAY STATUS QUERY FAILED: $e');
}
final blePeerId = reply.peerId?.trim() ?? '';
if (blePeerId.isNotEmpty) {
_log_('Lock PeerID from BLE: $blePeerId');
lock.peerId = blePeerId;
pairedLock.value = null;
pairedLock.value = lock;
unawaited(_waitForLockOnline(lock.lockId));
} else {
await _refreshLockPeerId(lock);
}
} catch (e) {
_log_('${S.logFailed}: $e');
} finally {
if (mounted) setState(() => _loading = false);
}
}
/// 配网成功后自动回查云端锁详情,回填锁版 peerId,并等待锁上线
Future<void> _refreshLockPeerId(scl.StarCloudLock lock) async {
try {
final fresh =
await StarCloudClient.instance.device.getDevice(lockId: lock.lockId);
pairedLock.value = fresh;
final p = (fresh.peerId ?? '').trim();
_log_(p.isNotEmpty
? 'Lock PeerID after config: $p'
: 'Lock not reported PeerID yet (may take a while).');
// 等待锁上线
unawaited(_waitForLockOnline(lock.lockId));
} catch (e) {
_log_('Refresh lock detail failed: $e (retry in Talk tab)');
}
}
/// 配网后轮询等待锁上线(最长等待 120 秒)
Future<void> _waitForLockOnline(int lockId) async {
const maxSeconds = 120;
const interval = Duration(seconds: 5);
int waited = 0;
_log_(S.isEn
? 'Waiting for lock to come online (this may take 30-60s)...'
: '等待锁上线(可能需要 30-60 秒)...');
while (waited < maxSeconds) {
await Future.delayed(interval);
waited += 5;
if (!mounted) return;
try {
final fresh =
await StarCloudClient.instance.device.getDevice(lockId: lockId);
final bool online = fresh.isOnline ?? false;
final String peerId = (fresh.peerId ?? '').trim();
if (online && peerId.isNotEmpty) {
pairedLock.value = fresh;
_log_(S.isEn
? 'SUCCESS: Lock is ONLINE! peerId=$peerId. You can start monitoring now.'
: '锁已上线!peerId=$peerId。现在可以开始监控了。');
return;
}
_log_(
'[${waited}s] ${S.isEn ? "still offline" : "仍未上线"} (peerId=${peerId.isNotEmpty ? peerId : "none"})');
} catch (e) {
_log_('Poll online status failed: $e');
}
}
_log_(S.isEn
? 'WARNING: Lock did not come online within 120s. Check WiFi signal and try restarting the lock.'
: '警告:120 秒内锁未上线。请检查 WiFi 信号,或尝试重启锁。');
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: S.langNotifier,
builder: (context, isEn, _) {
return ValueListenableBuilder<scl.StarCloudLock?>(
valueListenable: pairedLock,
builder: (context, lock, _) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ValueListenableBuilder<bool>(
valueListenable: videoTalkReady,
builder: (context, ready, _) {
return ValueListenableBuilder<bool>(
valueListenable: videoTalkInitializing,
builder: (context, initializing, _) {
return ElevatedButton.icon(
onPressed:
ready || initializing ? null : _initVideoTalk,
icon: initializing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.videocam),
label: Text(ready
? '${S.btnInitTalk} (${S.initialized})'
: S.btnInitTalk),
);
},
);
},
),
const SizedBox(height: 16),
Text(
lock != null
? '${S.pairedStatus}: ${lock.lockName.isNotEmpty ? lock.lockName : "ID:${lock.lockId}"}'
: S.notPaired,
style: TextStyle(
color: lock != null ? Colors.green : Colors.red,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
ValueListenableBuilder<String?>(
valueListenable: signedInAccount,
builder: (context, account, _) => Card(
elevation: 0,
color: account == null
? Colors.red.shade50
: Colors.green.shade50,
child: ListTile(
leading: Icon(account == null
? Icons.error_outline
: Icons.verified_user_outlined),
title: Text(account == null
? '未登录'
: '${S.currentLoggedInAccountPrefix}$account'),
subtitle: Text(account == null
? '请先在“登录”页登录锁主账号/管理员账号。'
: S.wifiUsesAuthorizedSession),
),
),
),
const SizedBox(height: 12),
TextField(
controller: _ssidCtrl,
decoration: InputDecoration(
labelText: isEn ? 'WiFi SSID' : 'WIFI名称',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.wifi))),
const SizedBox(height: 12),
TextField(
controller: _pwdCtrl,
obscureText: true,
decoration: InputDecoration(
labelText: isEn ? 'WiFi Password' : 'WIFI密码',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline))),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _loading || lock == null ? null : _config,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(50)),
icon: _loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Icon(Icons.send),
label: Text(_loading ? S.btnProcessing : S.btnStartConfig),
),
const SizedBox(height: 20),
LogArea(log: _log),
],
),
);
},
);
},
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class StatusChip extends StatelessWidget {
final String label;
final bool ready;
const StatusChip({super.key, required this.label, required this.ready});
@override
Widget build(BuildContext context) {
return Chip(
label: Text(label),
avatar: Icon(ready ? Icons.check_circle : Icons.radio_button_unchecked,
color: ready ? Colors.green : Colors.grey, size: 18),
backgroundColor: ready ? Colors.green.shade50 : Colors.grey.shade100);
}
}
class ActionButton extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
final bool loading;
final VoidCallback onTap;
const ActionButton(
{super.key,
required this.label,
required this.icon,
required this.color,
required this.loading,
required this.onTap});
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
onPressed: loading ? null : onTap,
icon: Icon(icon, color: color),
label: Text(label),
style: ElevatedButton.styleFrom(backgroundColor: color.withAlpha(25)));
}
}
class LogArea extends StatelessWidget {
final List<String> log;
const LogArea({super.key, required this.log});
@override
Widget build(BuildContext context) {
if (log.isEmpty) return const SizedBox.shrink();
return Container(
height: 120,
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
borderRadius: BorderRadius.circular(8)),
child: ListView.builder(
reverse: true,
itemCount: log.length,
itemBuilder: (_, i) => Text(log[log.length - 1 - i],
style: const TextStyle(
color: Color(0xFF4EC9B0),
fontSize: 11,
fontFamily: 'monospace'))),
);
}
}

749
example/pubspec.lock Executable file
View File

@ -0,0 +1,749 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.5"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.3"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.2"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.1"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
date_format:
dependency: transitive
description:
name: date_format
sha256: a48254e60bdb7f1d5a15cac7f86e37491808056c0a99dbdc850841def4754ddc
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.9"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.1.2"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.2"
dio:
dependency: transitive
description:
name: dio
sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.10.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
event_bus:
dependency: transitive
description:
name: event_bus
sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_blue_plus:
dependency: transitive
description:
name: flutter_blue_plus
sha256: "59fded880d769859e9e2fd6e5599555575174f642d758cc846988c86c4341131"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.33.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
flutter_pcm_sound:
dependency: "direct main"
description:
name: flutter_pcm_sound
sha256: e963b3a0c7658d57255f6e44fceddff6c7751dff82b7bcd561cfc1d5478d4e20
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.7"
flutter_sdk:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.1.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.2"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.4"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.3.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.0.13"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.4.10"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.3"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.9.1"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
record:
dependency: "direct main"
description:
name: record
sha256: "8cb57763d954624fbc673874930c6f1ceca3baaf9bfee24b25da6fd451362394"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.0"
record_android:
dependency: transitive
description:
name: record_android
sha256: "0b4739a2502fff402b0ac0ff1d6b2740854d116d78e06a4a16b3989821f84446"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
record_darwin:
dependency: transitive
description:
name: record_darwin
sha256: e487eccb19d82a9a39cd0126945cfc47b9986e0df211734e2788c95e3f63c82c
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
record_linux:
dependency: transitive
description:
name: record_linux
sha256: "74d41a9ebb1eb498a38e9a813dd524e8f0b4fdd627270bda9756f437b110a3e3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
record_platform_interface:
dependency: transitive
description:
name: record_platform_interface
sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
record_web:
dependency: transitive
description:
name: record_web
sha256: "10cb041349024ce4256e11dd35874df26d8b45b800678f2f51fd1318901adc64"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.4"
record_windows:
dependency: transitive
description:
name: record_windows
sha256: "7bce0ac47454212ca8bfa72791d8b6a951f2fb0d4b953b64443c014227f035b4"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.3"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
starchart:
dependency: transitive
description:
path: "../../starchart"
relative: true
source: path
version: "0.1.0"
starcloud:
dependency: "direct main"
description:
path: "../../starcloud"
relative: true
source: path
version: "0.1.2"
starlock_ble:
dependency: "direct main"
description:
path: "../../starlock_ble"
relative: true
source: path
version: "0.1.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
video_decode_plugin:
dependency: "direct main"
description:
path: "../../video_decode_plugin"
relative: true
source: path
version: "0.0.1"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.flutter-io.cn"
source: hosted
version: "13.0.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.5.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.3"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.19.0"

45
example/pubspec.yaml Executable file
View File

@ -0,0 +1,45 @@
name: flutter_sdk_example
description: Example application for flutter_sdk.
publish_to: 'none'
version: 0.1.0+1
environment:
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dependencies:
flutter:
sdk: flutter
flutter_sdk:
path: ..
starlock_ble:
path: ../../starlock_ble
starcloud:
path: ../../starcloud
crypto: ^3.0.3
shared_preferences: ^2.2.3
# 设备信息(用于视频对讲初始化)
device_info_plus: ^10.0.0
# 视频对讲:铃声(来电/呼叫/挂断)
audioplayers: ^5.2.1
# 视频对讲:PCM 音频播放(G711 解码后的 PCM 流)
flutter_pcm_sound: ^1.2.7
# 视频对讲:麦克风录音(PCM 流采集)
record: ^5.1.2
# 视频对讲:权限申请
permission_handler: ^11.3.1
# 视频对讲:H264 裸流解码渲染(本地插件,含 ffmpeg-kit)
video_decode_plugin:
path: ../../video_decode_plugin
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

View File

@ -0,0 +1,57 @@
enum StarCloudLockFeatureBit {
remoteUnlock(28),
autoLock(29),
antiPryAlarm(30),
resetButton(31),
lockSound(33),
passageMode(50),
appUnlockOnline(55),
bluetoothBroadcast(56),
stayWarn(61),
abnormalWarn(62);
final int value;
const StarCloudLockFeatureBit(this.value);
}
class StarCloudLockSettingRequest {
final String lockId;
final String keyId;
final String userId;
final List<int> token;
final int needAuthor;
final List<int> signKey;
final List<int> privateKey;
const StarCloudLockSettingRequest({
required this.lockId,
required this.keyId,
required this.userId,
required this.token,
required this.needAuthor,
required this.signKey,
required this.privateKey,
});
}
class StarCloudFeatureToggleRequest {
final String keyId;
final String userId;
final int featureBit;
final int featureEnable;
final List<int> token;
final int needAuthor;
final List<int> publicKey;
final List<int> privateKey;
const StarCloudFeatureToggleRequest({
required this.keyId,
required this.userId,
required this.featureBit,
required this.featureEnable,
required this.token,
required this.needAuthor,
required this.publicKey,
required this.privateKey,
});
}

View File

@ -0,0 +1,15 @@
class StarCloudLockStateRequest {
final String lockId;
final String userId;
final int utcTimeStamp;
final int unixTimeStamp;
final List<int> privateKey;
const StarCloudLockStateRequest({
required this.lockId,
required this.userId,
required this.utcTimeStamp,
required this.unixTimeStamp,
required this.privateKey,
});
}

View File

@ -0,0 +1,91 @@
class StarCloudWifiSearchRequest {
final String keyId;
final String userId;
final List<int> token;
final int needAuthor;
final List<int> signKey;
/// 锁公钥。WiFi 直连配网认证摘要使用此字段。
final List<int>? publicKey;
final List<int> privateKey;
/// 锁的蓝牙设备名(如锁名),用于连接前定位设备
final String deviceName;
/// 锁的蓝牙 MAC 地址,Android 优先使用
final String mac;
const StarCloudWifiSearchRequest({
required this.keyId,
required this.userId,
required this.token,
required this.needAuthor,
required this.signKey,
this.publicKey,
required this.privateKey,
this.deviceName = '',
this.mac = '',
});
}
class StarCloudWifiConfigRequest {
final String keyId;
final String userId;
final String ssid;
final String password;
final String peerId;
final int numberOfServers;
final List<int> listOfServers;
final int numberOfPhone;
final List<String> listOfPhone;
final List<int> token;
final int needAuthor;
final List<int> signKey;
/// 锁公钥。WiFi 直连配网认证摘要使用此字段。
final List<int>? publicKey;
final List<int> privateKey;
final String? gatewayConfigurationStr;
/// 锁的蓝牙设备名(如锁名),用于连接前定位设备
final String deviceName;
/// 锁的蓝牙 MAC 地址,Android 优先使用
final String mac;
const StarCloudWifiConfigRequest({
required this.keyId,
required this.userId,
required this.ssid,
required this.password,
required this.peerId,
required this.numberOfServers,
required this.listOfServers,
required this.numberOfPhone,
required this.listOfPhone,
required this.token,
required this.needAuthor,
required this.signKey,
this.publicKey,
required this.privateKey,
this.gatewayConfigurationStr,
this.deviceName = '',
this.mac = '',
});
}
class WifiInfo {
final String ssid;
final int signal;
final int security;
const WifiInfo({
required this.ssid,
required this.signal,
required this.security,
});
@override
String toString() =>
'WifiInfo{ssid: $ssid, signal: $signal, security: $security}';
}

View File

@ -0,0 +1,10 @@
class StarCloudProtocolException implements Exception {
final String message;
final Object? raw;
const StarCloudProtocolException(this.message, {this.raw});
@override
String toString() =>
'StarCloudProtocolException(message: $message, raw: $raw)';
}

View File

@ -0,0 +1,9 @@
import 'package:starlock_ble/starlock_ble.dart';
abstract class StarCloudProtocolExecutor {
Future<T> executeForReply<T extends Reply>(
SenderProtocol command, {
Duration timeout,
bool Function(T reply)? test,
});
}

View File

@ -0,0 +1,45 @@
import 'dart:async';
import 'package:starlock_ble/starlock_ble.dart';
import 'starcloud_protocol_executor.dart';
class StarCloudProtocolExecutorImpl implements StarCloudProtocolExecutor {
@override
Future<T> executeForReply<T extends Reply>(
SenderProtocol command, {
Duration? timeout,
bool Function(T reply)? test,
}) async {
final sdk = StarLockBLE.instance;
// send the command
await sdk.send(command);
// listen for replies on the shared EventBus and apply optional predicate
final bus = EventBusManager().bus;
if (bus == null) {
throw StateError('EventBus is not initialized');
}
final completer = Completer<T>();
final sub = bus.on<Reply>().listen((reply) {
if (reply is T) {
final tReply = reply as T;
if (test == null || test(tReply)) {
if (!completer.isCompleted) {
completer.complete(tReply);
}
}
}
});
try {
return await completer.future.timeout(timeout ?? sdk.timeout, onTimeout: () {
throw TimeoutException('Timeout waiting for reply: ${T.toString()}');
});
} finally {
await sub.cancel();
}
}
}

View File

@ -0,0 +1,417 @@
import 'package:starlock_ble/starlock_ble.dart';
import '../core/starcloud_lock_context.dart';
import 'models/lock_setting_models.dart';
import 'protocol/starcloud_protocol_executor.dart';
class StarCloudLockSettingService {
StarCloudLockSettingService(this._executor);
final StarCloudProtocolExecutor _executor;
UpdataLockSetCommand buildUploadLockSettingCommand(
StarCloudLockSettingRequest request,
) {
return UpdataLockSetCommand(
lockID: request.lockId,
keyID: request.keyId,
userID: request.userId,
token: request.token,
needAuthor: request.needAuthor,
signKey: request.signKey,
privateKey: request.privateKey,
);
}
Future<UpdataLockSetReply> uploadLockSetting(
StarCloudLockSettingRequest request, {
Duration timeout = const Duration(seconds: 15),
}) {
return _executor.executeForReply<UpdataLockSetReply>(
buildUploadLockSettingCommand(request),
timeout: timeout,
);
}
ReadSupportFunctionsNoParametersCommand buildReadFeatureCommand(
StarCloudFeatureToggleRequest request,
) {
return ReadSupportFunctionsNoParametersCommand(
keyID: request.keyId,
userID: request.userId,
featureBit: request.featureBit,
token: request.token,
needAuthor: request.needAuthor,
publicKey: request.publicKey,
privateKey: request.privateKey,
);
}
Future<ReadSupportFunctionsNoParametersReply> readFeature(
StarCloudFeatureToggleRequest request, {
Duration timeout = const Duration(seconds: 15),
}) {
return _executor.executeForReply<ReadSupportFunctionsNoParametersReply>(
buildReadFeatureCommand(request),
timeout: timeout,
);
}
SetSupportFunctionsNoParametersCommand buildSetFeatureCommand(
StarCloudFeatureToggleRequest request,
) {
return SetSupportFunctionsNoParametersCommand(
keyID: request.keyId,
userID: request.userId,
featureBit: request.featureBit,
featureEnable: request.featureEnable,
token: request.token,
needAuthor: request.needAuthor,
publicKey: request.publicKey,
privateKey: request.privateKey,
);
}
Future<SetSupportFunctionsNoParametersReply> setFeature(
StarCloudFeatureToggleRequest request, {
Duration timeout = const Duration(seconds: 15),
}) {
return _executor.executeForReply<SetSupportFunctionsNoParametersReply>(
buildSetFeatureCommand(request),
timeout: timeout,
);
}
ReadSupportFunctionsWithParametersCommand
buildReadFeatureWithParametersCommand(
StarCloudFeatureToggleRequest request,
) {
return ReadSupportFunctionsWithParametersCommand(
keyID: request.keyId,
userID: request.userId,
featureBit: request.featureBit,
token: request.token,
needAuthor: request.needAuthor,
publicKey: request.publicKey,
privateKey: request.privateKey,
);
}
Future<ReadSupportFunctionsWithParametersReply> readFeatureWithParameters(
StarCloudFeatureToggleRequest request, {
Duration timeout = const Duration(seconds: 15),
}) {
return _executor.executeForReply<ReadSupportFunctionsWithParametersReply>(
buildReadFeatureWithParametersCommand(request),
timeout: timeout,
);
}
SetSupportFunctionsWithParametersCommand
buildSetFeatureWithParametersCommand({
required String keyId,
required String userId,
required int featureBit,
required int featureParaLength,
required List<int> featureData,
required List<int> token,
required int needAuthor,
required List<int> publicKey,
required List<int> privateKey,
}) {
return SetSupportFunctionsWithParametersCommand(
keyID: keyId,
userID: userId,
featureBit: featureBit,
featureParaLength: featureParaLength,
featureData: featureData,
token: token,
needAuthor: needAuthor,
publicKey: publicKey,
privateKey: privateKey,
);
}
Future<SetSupportFunctionsWithParametersReply> setFeatureWithParameters({
required String keyId,
required String userId,
required int featureBit,
required int featureParaLength,
required List<int> featureData,
required List<int> token,
required int needAuthor,
required List<int> publicKey,
required List<int> privateKey,
Duration timeout = const Duration(seconds: 15),
}) {
return _executor.executeForReply<SetSupportFunctionsWithParametersReply>(
buildSetFeatureWithParametersCommand(
keyId: keyId,
userId: userId,
featureBit: featureBit,
featureParaLength: featureParaLength,
featureData: featureData,
token: token,
needAuthor: needAuthor,
publicKey: publicKey,
privateKey: privateKey,
),
timeout: timeout,
);
}
Future<SetSupportFunctionsNoParametersReply> setRemoteUnlock({
required String keyId,
required String userId,
required bool enabled,
required List<int> token,
required int needAuthor,
required List<int> publicKey,
required List<int> privateKey,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeature(
StarCloudFeatureToggleRequest(
keyId: keyId,
userId: userId,
featureBit: StarCloudLockFeatureBit.remoteUnlock.value,
featureEnable: enabled ? 1 : 0,
token: token,
needAuthor: needAuthor,
publicKey: publicKey,
privateKey: privateKey,
),
timeout: timeout,
);
}
Future<SetSupportFunctionsNoParametersReply> setAppUnlockOnline({
required String keyId,
required String userId,
required bool enabled,
required List<int> token,
required int needAuthor,
required List<int> publicKey,
required List<int> privateKey,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeature(
StarCloudFeatureToggleRequest(
keyId: keyId,
userId: userId,
featureBit: StarCloudLockFeatureBit.appUnlockOnline.value,
featureEnable: enabled ? 1 : 0,
token: token,
needAuthor: needAuthor,
publicKey: publicKey,
privateKey: privateKey,
),
timeout: timeout,
);
}
Future<SetSupportFunctionsNoParametersReply> setBluetoothBroadcast({
required String keyId,
required String userId,
required bool enabled,
required List<int> token,
required int needAuthor,
required List<int> publicKey,
required List<int> privateKey,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeature(
StarCloudFeatureToggleRequest(
keyId: keyId,
userId: userId,
featureBit: StarCloudLockFeatureBit.bluetoothBroadcast.value,
featureEnable: enabled ? 1 : 0,
token: token,
needAuthor: needAuthor,
publicKey: publicKey,
privateKey: privateKey,
),
timeout: timeout,
);
}
// ---------------------------------------------------------------------------
// ByContext helpers — auth fields are filled automatically from the context.
// ---------------------------------------------------------------------------
/// Simplified variant — auth fields are filled from [context].
///
/// ```dart
/// await StarCloudClient.instance.lockSetting
/// .uploadLockSettingByContext(context);
/// ```
Future<UpdataLockSetReply> uploadLockSettingByContext(
StarCloudLockContext context, {
Duration timeout = const Duration(seconds: 15),
}) {
return uploadLockSetting(
StarCloudLockSettingRequest(
lockId: context.lockId.toString(),
keyId: context.keyId,
userId: context.userId,
token: context.token,
needAuthor: context.needAuthor,
signKey: context.signKey,
privateKey: context.privateKey,
),
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
///
/// ```dart
/// final reply = await StarCloudClient.instance.lockSetting
/// .readFeatureByContext(context, featureBit: StarCloudLockFeatureBit.remoteUnlock);
/// ```
Future<ReadSupportFunctionsNoParametersReply> readFeatureByContext(
StarCloudLockContext context, {
required StarCloudLockFeatureBit featureBit,
Duration timeout = const Duration(seconds: 15),
}) {
return readFeature(
StarCloudFeatureToggleRequest(
keyId: context.keyId,
userId: context.userId,
featureBit: featureBit.value,
featureEnable: 0,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
),
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
///
/// ```dart
/// await StarCloudClient.instance.lockSetting.setFeatureByContext(
/// context,
/// featureBit: StarCloudLockFeatureBit.remoteUnlock,
/// enabled: true,
/// );
/// ```
Future<SetSupportFunctionsNoParametersReply> setFeatureByContext(
StarCloudLockContext context, {
required StarCloudLockFeatureBit featureBit,
required bool enabled,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeature(
StarCloudFeatureToggleRequest(
keyId: context.keyId,
userId: context.userId,
featureBit: featureBit.value,
featureEnable: enabled ? 1 : 0,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
),
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
Future<ReadSupportFunctionsWithParametersReply>
readFeatureWithParametersByContext(
StarCloudLockContext context, {
required StarCloudLockFeatureBit featureBit,
Duration timeout = const Duration(seconds: 15),
}) {
return readFeatureWithParameters(
StarCloudFeatureToggleRequest(
keyId: context.keyId,
userId: context.userId,
featureBit: featureBit.value,
featureEnable: 0,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
),
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
///
/// ```dart
/// await StarCloudClient.instance.lockSetting.setFeatureWithParametersByContext(
/// context,
/// featureBit: StarCloudLockFeatureBit.passageMode,
/// featureParaLength: 2,
/// featureData: [1, 0],
/// );
/// ```
Future<SetSupportFunctionsWithParametersReply>
setFeatureWithParametersByContext(
StarCloudLockContext context, {
required StarCloudLockFeatureBit featureBit,
required int featureParaLength,
required List<int> featureData,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeatureWithParameters(
keyId: context.keyId,
userId: context.userId,
featureBit: featureBit.value,
featureParaLength: featureParaLength,
featureData: featureData,
token: context.token,
needAuthor: context.needAuthor,
publicKey: context.publicKey,
privateKey: context.privateKey,
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
Future<SetSupportFunctionsNoParametersReply> setRemoteUnlockByContext(
StarCloudLockContext context, {
required bool enabled,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeatureByContext(
context,
featureBit: StarCloudLockFeatureBit.remoteUnlock,
enabled: enabled,
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
Future<SetSupportFunctionsNoParametersReply> setAppUnlockOnlineByContext(
StarCloudLockContext context, {
required bool enabled,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeatureByContext(
context,
featureBit: StarCloudLockFeatureBit.appUnlockOnline,
enabled: enabled,
timeout: timeout,
);
}
/// Simplified variant — auth fields are filled from [context].
Future<SetSupportFunctionsNoParametersReply> setBluetoothBroadcastByContext(
StarCloudLockContext context, {
required bool enabled,
Duration timeout = const Duration(seconds: 15),
}) {
return setFeatureByContext(
context,
featureBit: StarCloudLockFeatureBit.bluetoothBroadcast,
enabled: enabled,
timeout: timeout,
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:starcloud/blue/blue_read_write.dart';
import 'package:starcloud/device/ble/protocol/public/lock_status_info.dart';
import '../core/starcloud_exception.dart';
import '../core/starcloud_lock_context.dart';
import 'models/lock_state_models.dart';
class StarCloudLockStateService {
StarCloudLockStateService();
LockStatusInfoCommand _buildReadLockStateCommand(
StarCloudLockStateRequest request,
) {
return LockStatusInfoCommand(
lockID: request.lockId,
userID: request.userId,
utcTimeStamp: request.utcTimeStamp,
unixTimeStamp: request.unixTimeStamp,
privateKey: request.privateKey,
);
}
List<int> buildReadLockStatePayload(
StarCloudLockStateRequest request,
) {
return _buildReadLockStateCommand(request).packageData();
}
Future<GetStarLockStatusInfoReply> readLockState(
StarCloudLockStateRequest request, {
Duration timeout = const Duration(seconds: 15),
}) async {
final command = _buildReadLockStateCommand(request);
try {
final reply = await BlueReadWrite.shareManager()!.writeAndReturn(
command,
getPrivateKey: () => request.privateKey,
);
return reply as GetStarLockStatusInfoReply;
} catch (e) {
throw StarCloudException(
'Read lock state failed',
raw: e,
);
}
}
Future<GetStarLockStatusInfoReply> readLockStateByContext(
StarCloudLockContext context, {
Duration timeout = const Duration(seconds: 15),
}) {
final now = DateTime.now();
return readLockState(
StarCloudLockStateRequest(
lockId: context.lockId.toString(),
userId: context.userId,
utcTimeStamp: now.toUtc().millisecondsSinceEpoch ~/ 1000,
unixTimeStamp: now.millisecondsSinceEpoch ~/ 1000,
privateKey: context.privateKey,
),
timeout: timeout,
);
}
}

View File

@ -0,0 +1,656 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:starcloud/blue/blue_features.dart';
import 'package:starcloud/blue/blue_read_write.dart';
import 'package:starcloud/blue/entity/blue_message.dart';
import 'package:starcloud/device/ble/business/ble_type.dart';
import 'package:starcloud/device/ble/protocol/public/configuring_wifi.dart';
import 'package:starcloud/device/ble/protocol/public/gateway_configuring_wifi.dart';
import 'package:starcloud/device/ble/protocol/public/gateway_get_status.dart';
import 'package:starcloud/device/ble/protocol/public/get_wifi_list.dart';
import 'package:starcloud/net/entity/api_response.dart';
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/business/sdk_wrapper.dart';
import 'package:starchart/src/discovery/discovery_service.dart';
import '../core/starcloud_exception.dart';
import '../core/starcloud_lock_context.dart';
import '../starcloud_client.dart';
import 'models/wifi_models.dart' as wifi_models;
class StarCloudWifiService {
StarCloudWifiService();
/// 配网/搜 WiFi 前必须设置锁的蓝牙设备名与 MAC,
/// 否则 BlueReadWrite 单例为空会走空名搜索 → 15s 超时 → 10004 蓝牙请求超时。
///
/// 注意:Android 12+ 蓝牙使用随机地址,云端锁详情里的静态 MAC 无法
/// `BluetoothDevice.fromId()` 直连(实测 10s connect 超时),因此
/// Android 一律只设置设备名、走扫描路径(_searchByDeviceName);
/// 仅 iOS 使用 MAC 直连。
BlueReadWrite _prepareBlueRW({String deviceName = '', String mac = ''}) {
final blueRW = BlueReadWrite.shareManager();
if (blueRW == null) {
throw StarCloudException(
'Bluetooth service is unavailable',
raw: 'BlueReadWrite manager is null',
);
}
if (deviceName.isNotEmpty) blueRW.deviceName = deviceName;
if (!Platform.isAndroid && mac.isNotEmpty) blueRW.mac = mac;
blueRW.type = BlueFeaturesType.lock;
return blueRW;
}
GetWifiListCommand _buildSearchCommand(
wifi_models.StarCloudWifiSearchRequest request) {
return GetWifiListCommand(
userID: request.userId,
privateKey: request.privateKey,
);
}
GatewayConfiguringWifiCommand _buildConfigCommand(
wifi_models.StarCloudWifiConfigRequest request) {
return GatewayConfiguringWifiCommand(
ssid: request.ssid,
password: request.password,
gatewayConfigurationStr: request.gatewayConfigurationStr ?? '',
);
}
Future<List<wifi_models.WifiInfo>> searchWifiAndGetList(
wifi_models.StarCloudWifiSearchRequest request, {
Duration timeout = const Duration(seconds: 20),
}) async {
final blueRW = _prepareBlueRW(
deviceName: request.deviceName,
mac: request.mac,
);
final command = _buildSearchCommand(request);
final completer = Completer<List<wifi_models.WifiInfo>>();
final List<wifi_models.WifiInfo> accumulated = [];
Timer? timeoutTimer;
Timer? settleTimer;
StreamSubscription<BlueMessage>? messageSub;
void complete() {
timeoutTimer?.cancel();
settleTimer?.cancel();
messageSub?.cancel();
if (!completer.isCompleted) {
completer.complete(List.unmodifiable(accumulated));
}
}
void cleanup() {
timeoutTimer?.cancel();
settleTimer?.cancel();
messageSub?.cancel();
}
messageSub = blueRW.messageStream.listen((BlueMessage message) {
if (message.data.length < 2) return;
final int cmdType = message.data[0] * 256 + message.data[1];
if (cmdType == CommandType.gatewayGetWifiListResult.typeValue) {
final resultReply = GetWifiListResultReply.parseData(
CommandType.gatewayGetWifiListResult,
message.data,
);
final entries = resultReply.wifiList
.map((m) => wifi_models.WifiInfo(
ssid: m['ssid'] as String,
signal: m['signal'] as int,
security: m['security'] as int,
))
.where((w) => w.ssid.isNotEmpty)
.toList();
accumulated.addAll(entries);
// 重置结算计时器:在最后一条结果消息到达后 1.5 秒完成
settleTimer?.cancel();
settleTimer = Timer(const Duration(milliseconds: 1500), complete);
}
});
timeoutTimer = Timer(timeout, () {
cleanup();
if (!completer.isCompleted) {
if (accumulated.isNotEmpty) {
completer.complete(List.unmodifiable(accumulated));
} else {
completer.completeError(
StarCloudException(
'WiFi list search timeout',
raw: 'timeout',
),
);
}
}
});
try {
await blueRW.writeAndReturn(
command,
getPrivateKey: () => request.privateKey,
);
} catch (e) {
cleanup();
if (!completer.isCompleted) {
completer.completeError(
StarCloudException('WiFi search failed', raw: e),
);
}
}
return completer.future;
}
Future<GatewayConfiguringWifiResultReply> configureWifi(
wifi_models.StarCloudWifiConfigRequest request, {
Duration timeout = const Duration(seconds: 120),
}) async {
print(
'[WifiConfig] start ssid=${request.ssid} deviceName=${request.deviceName} mac=${request.mac} configLen=${request.gatewayConfigurationStr?.length ?? 0}');
final blueRW = _prepareBlueRW(
deviceName: request.deviceName,
mac: request.mac,
);
final command = _buildConfigCommand(request);
// 清除上一个业务残留的回调,防止 0x30F4/0x30F5 数据被错误格式解析(RangeError / 误识别)
blueRW.clearCallback();
final completer = Completer<GatewayConfiguringWifiResultReply>();
Timer? timer;
Timer? reconnectTimer;
StreamSubscription<BlueMessage>? messageSub;
void cleanup() {
timer?.cancel();
reconnectTimer?.cancel();
messageSub?.cancel();
blueRW.clearCallback();
}
// 监听配网结果(0x30F5)
messageSub = blueRW.messageStream.listen((BlueMessage message) {
if (message.data.length < 2) return;
final int cmdType = message.data[0] * 256 + message.data[1];
if (cmdType == CommandType.gatewayConfiguringWifiResult.typeValue) {
cleanup();
if (!completer.isCompleted) {
final reply = GatewayConfiguringWifiResultReply.parseData(
CommandType.gatewayConfiguringWifiResult,
message.data,
);
completer.complete(reply);
}
}
});
timer = Timer(timeout, () {
cleanup();
if (!completer.isCompleted) {
completer.completeError(
StarCloudException(
'WiFi configuration timeout after ${timeout.inSeconds}s',
raw: 'timeout',
),
);
}
});
try {
final ackReply = await blueRW.writeAndReturn(
command,
getPrivateKey: () => request.privateKey,
);
if (ackReply.status != 0x00) {
cleanup();
if (!completer.isCompleted) {
completer.completeError(
StarCloudException(
'WiFi configuration rejected by device',
raw: 'status=0x${ackReply.status.toRadixString(16)}',
),
);
}
}
void scheduleReconnect() {
reconnectTimer = Timer(const Duration(seconds: 5), () async {
if (completer.isCompleted) return;
try {
await blueRW.reconnect(isNeedSeacrch: true);
scheduleReconnect();
} catch (_) {
scheduleReconnect();
}
});
}
scheduleReconnect();
} catch (e) {
print('[WifiConfig] writeAndReturn failed: $e');
cleanup();
if (!completer.isCompleted) {
completer.completeError(
StarCloudException('WiFi configuration failed', raw: e),
);
}
}
return completer.future;
}
Future<GatewayConfiguringWifiResultReply> configureWifiByContext(
StarCloudLockContext context, {
required String ssid,
required String password,
required String lockServiceAccessToken,
String? gatewayConfigurationStr,
String deviceName = '',
String mac = '',
Duration timeout = const Duration(seconds: 120),
}) async {
final String configStr =
(gatewayConfigurationStr != null && gatewayConfigurationStr.isNotEmpty)
? gatewayConfigurationStr
: await _fetchGatewayConfiguration(
context.userId,
lockServiceAccessToken,
);
final GatewayConfiguringWifiResultReply reply = await configureWifi(
wifi_models.StarCloudWifiConfigRequest(
keyId: context.keyId,
userId: context.userId,
ssid: ssid,
password: password,
peerId: '',
numberOfServers: 0,
listOfServers: <int>[],
numberOfPhone: 0,
listOfPhone: <String>[],
token: context.token,
needAuthor: context.needAuthor,
signKey: context.signKey,
privateKey: context.privateKey,
gatewayConfigurationStr: configStr,
deviceName: deviceName,
mac: mac,
),
timeout: timeout,
);
if (reply.status != 0x00) {
throw StarCloudException(
'Gateway WiFi configuration rejected by device',
raw: 'status=0x${reply.status.toRadixString(16)}',
);
}
final String peerId = reply.peerId?.trim() ?? '';
final String wifiName = (reply.wifiName?.trim().isNotEmpty ?? false)
? reply.wifiName!.trim()
: ssid;
final String secretKey = reply.secretKey?.trim() ?? '';
final String deviceMac = (reply.deviceMac?.trim().isNotEmpty ?? false)
? reply.deviceMac!.trim()
: mac;
final String networkMac = reply.networkMac?.trim() ?? '';
// 与原始 App 保持一致:设备固件只保证 peerId 和 secretKey 必有值。
// 其余字段在部分固件中会缺失,提交空值或客户端已知值即可。
if (peerId.isEmpty || secretKey.isEmpty) {
throw StarCloudException(
'BLE 0x30F5 response is missing required peerId or secretKey',
raw: <String, dynamic>{
'payload': reply.payloadJson,
'peerId': peerId,
'wifiName': wifiName,
'secretKeyPresent': secretKey.isNotEmpty,
'deviceMac': deviceMac,
'networkMac': networkMac,
'rssi': reply.rssi,
'bytes': reply.data,
},
);
}
print('[WifiConfig] 0x30F5 parsed peerId=$peerId wifiName=$wifiName '
'deviceMac=$deviceMac networkMac=$networkMac rssi=${reply.rssi ?? 0}');
final ApiResponse settingResponse =
await SDKCommon.instance.netApi.deviceApi.settingDeviceNetwork(
lockServiceAccessToken: lockServiceAccessToken,
deviceType: 2,
deviceMac: deviceMac,
wifiName: wifiName,
networkMac: networkMac,
secretKey: secretKey,
peerId: peerId,
rssi: reply.rssi ?? 0,
);
if (settingResponse.errcode != 0) {
throw StarCloudException(
'WiFi succeeded over BLE, but deviceNetwork/setting failed: '
'${settingResponse.errmsg}',
raw: settingResponse,
);
}
return reply;
}
/// 查询网关是否已接受网络配置并完成联网初始化。
///
/// 0x30F4/0x30F5 只表示配网流程被接受,不能作为网关已上线的依据;
/// 参考项目在此之后使用 0x30F8 查询状态。
Future<GatewayGetStatusReply> getGatewayStatus({
required String lockId,
required String userId,
String deviceName = '',
String mac = '',
}) async {
final blueRW = _prepareBlueRW(deviceName: deviceName, mac: mac);
blueRW.clearCallback();
try {
final reply = await blueRW.writeAndReturn(
GatewayGetStatusCommand(lockId: lockId, userId: userId),
);
return reply as GatewayGetStatusReply;
} catch (e) {
throw StarCloudException('Gateway status query failed', raw: e);
} finally {
blueRW.clearCallback();
}
}
Future<String> _fetchGatewayConfiguration(
String userId,
String lockServiceAccessToken,
) async {
final String userPeerId = await _getStarChartPeerId(userId);
try {
final ApiResponse response = await SDKCommon.instance.netApi.deviceApi
.getGatewayConfig(lockServiceAccessToken: lockServiceAccessToken);
if (response.errcode == 0 && response.data != null) {
final Map<String, dynamic> jsonMap;
if (response.data is Map) {
jsonMap = Map<String, dynamic>.from(response.data as Map);
} else {
jsonMap =
json.decode(response.data.toString()) as Map<String, dynamic>;
}
print('[WifiConfig] gateway config keys: ${jsonMap.keys.toList()}');
// 保留所有云端返回的 URL/服务器配置字段(cloud/starCloudUrl 等),
// 锁端需要这些地址才能连接中继服务器上线。
// 仅移除锁端不应直接使用的锁 Peer 字段。
jsonMap.remove('starLockPeerId');
// 修复:部分云端返回的 URL 字段值被反引号包裹(如 `http://xxx`),
// 锁端无法正确解析,需要清理掉这些反引号。
// 同时处理常见的 URL 字段:starcloudUrl, starCloudUrl, scdUrl 等。
const urlKeys = <String>[
'starcloudUrl',
'starCloudUrl',
'scdUrl',
'cloudUrl',
'discoveryUrl',
'starchartUrl',
];
for (final key in urlKeys) {
final dynamic value = jsonMap[key];
if (value is String && value.isNotEmpty) {
String cleaned = value;
// 移除首尾的反引号
if (cleaned.startsWith('`')) {
cleaned = cleaned.substring(1);
}
if (cleaned.endsWith('`')) {
cleaned = cleaned.substring(0, cleaned.length - 1);
}
if (cleaned != value) {
jsonMap[key] = cleaned;
print(
'[WifiConfig] cleaned URL field $key: "$value" -> "$cleaned"');
}
}
}
// 云端返回的 userPeerId 当前可能为空,统一设置为 App 的 peerId,
// 确保锁端知道该呼叫谁。
jsonMap['userPeerId'] = userPeerId;
jsonMap['userPeerld'] = userPeerId;
final bool hasServerConfig = jsonMap.keys.any((key) {
final String normalized = key.toLowerCase();
return normalized.contains('url') ||
normalized.contains('server') ||
normalized.contains('relay');
});
if (!hasServerConfig) {
throw StarCloudException(
'Gateway configuration is incomplete: no cloud or relay server fields',
);
}
final String configStr = json.encode(jsonMap);
print(
'[WifiConfig] getGatewayConfig OK, configLen=${configStr.length}, '
'keys=${jsonMap.keys.toList()}');
return configStr;
} else {
print(
'[WifiConfig] getGatewayConfig errcode=${response.errcode} msg=${response.errmsg}');
}
} catch (e, s) {
print('[WifiConfig] getGatewayConfig failed: $e\n$s');
}
throw StarCloudException(
'Unable to obtain complete gateway configuration from cloud',
);
}
Future<String> _getStarChartPeerId(String userId) async {
// 优先复用已初始化对讲的稳定 peerId,避免注册出第二个星图节点
// (新实例 StarChartClient().talk.fromPeerId 在未 init 时恒为空,必须走已初始化的单例)
try {
final String myPeerId = StarCloudClient.instance.talk.myPeerId;
if (myPeerId.isNotEmpty) return myPeerId;
} catch (_) {}
try {
final cred = await DiscoveryService().register(
productName: 'StarCloudSDK',
unique: 'uid_$userId',
);
return cred.id;
} catch (_) {
return userId;
}
}
// ── 锁板直连WiFi配网 ──
Future<ConfiguringWifiReply> configureLockWifi(
wifi_models.StarCloudWifiConfigRequest request, {
Duration timeout = const Duration(seconds: 120),
}) async {
_prepareBlueRW(deviceName: request.deviceName, mac: request.mac);
final command = ConfiguringWifiCommand(
keyID: request.keyId,
userID: request.userId,
ssid: request.ssid,
password: request.password,
peerId: request.peerId,
numberOfServers: request.numberOfServers,
listOfServers: request.listOfServers,
numberOfPhone: request.numberOfPhone,
listOfPhone: request.listOfPhone,
token: request.token,
needAuthor: request.needAuthor,
// WiFi 配网认证摘要按协议使用 publicKey;request.signKey 是历史字段名,
// 为保持 API 兼容仍保留该字段,但这里传入锁的 publicKey。
publicKey: request.publicKey ?? request.signKey,
privateKey: request.privateKey,
);
final blueRW = BlueReadWrite.shareManager()!;
try {
final reply = await blueRW.writeAndReturn(
command,
getPrivateKey: () => request.privateKey,
timeout: timeout,
);
final result = reply as ConfiguringWifiReply;
if (result.status != 0x00) {
throw StarCloudException(
'Lock WiFi configuration rejected by device',
raw: 'status=0x${result.status.toRadixString(16)}',
);
}
return result;
} catch (e) {
throw StarCloudException('Lock WiFi configuration failed', raw: e);
}
}
/// 通过蓝牙直接给锁板配置WiFi。
/// [peerId] 为 App 的星图 ID,必须传入,否则门铃无法呼叫。
/// [deviceName] / [mac] 为锁的蓝牙设备名与 MAC,必须传入,否则连接超时(10004)。
Future<ConfiguringWifiReply> configureLockWifiByContext(
StarCloudLockContext context, {
required String ssid,
required String password,
/// Token returned by `https://lock.skychip.top/api/user/login`.
///
/// This is intentionally separate from the OAuth token issued by
/// `cloud.skychip.top/sdk/oauth2/token`; the two services use different
/// authentication domains.
String? lockServiceAccessToken,
String peerId = '',
String deviceName = '',
String mac = '',
Duration timeout = const Duration(seconds: 120),
}) async {
final List<int> servers = await _fetchLockWifiServers(
context,
lockServiceAccessToken: lockServiceAccessToken,
);
final int numberOfServers = servers.length ~/ 6;
if (numberOfServers == 0) {
throw StarCloudException(
'No WiFi lock server is available from cloud configuration',
);
}
final ConfiguringWifiReply reply = await configureLockWifi(
wifi_models.StarCloudWifiConfigRequest(
keyId: context.keyId,
userId: context.userId,
ssid: ssid,
password: password,
peerId: peerId,
numberOfServers: numberOfServers,
listOfServers: servers,
numberOfPhone: 1,
listOfPhone: <String>[context.userId],
token: context.token,
needAuthor: context.needAuthor,
// 保留历史字段,实际直连配网摘要使用下方的 publicKey。
signKey: context.signKey,
// 直连 WiFi 配网协议要求使用 publicKey 计算认证摘要。
publicKey: context.publicKey,
privateKey: context.privateKey,
deviceName: deviceName,
mac: mac,
),
timeout: timeout,
);
final ApiResponse updateResponse =
await SDKCommon.instance.netApi.deviceApi.updateLockNetwork(
lockId: context.lockId,
network: ssid,
clientId: SDKCommon.instance.user.clientId,
);
if (updateResponse.errcode != 0) {
throw StarCloudException(
'WiFi was accepted by the lock, but cloud registration failed',
raw: updateResponse,
);
}
return reply;
}
Future<List<int>> _fetchLockWifiServers(
StarCloudLockContext context, {
String? lockServiceAccessToken,
}) async {
print('[WifiConfig] fetching WiFi lock servers from cloud...');
try {
if (lockServiceAccessToken == null || lockServiceAccessToken.isEmpty) {
throw StarCloudException(
'A lock.skychip.top service access token is required for WiFi configuration. '
'It must come from /api/user/login and cannot be replaced by the cloud OAuth token.',
);
}
final SdkWrapper cloudUser =
await SDKCommon.instance.fetchStarCloudUser(context.cloudUid);
SDKCommon.instance.netApi.switchCloudAccount(cloudUser.user);
final ApiResponse response =
await SDKCommon.instance.netApi.deviceApi.getWifiServiceIp(
lockServiceAccessToken: lockServiceAccessToken,
);
print(
'[WifiConfig] getWifiServiceIp response: errcode=${response.errcode}');
if (response.errcode != 0 || response.data is! Map) {
print(
'[WifiConfig] ERROR: getWifiServiceIp failed, errcode=${response.errcode}, errmsg=${response.errmsg}');
final String message =
'Unable to obtain WiFi lock server configuration: '
'${response.errmsg.isNotEmpty ? response.errmsg : response.errcode}';
throw StarCloudException(message, raw: response);
}
final Map<String, dynamic> data =
Map<String, dynamic>.from(response.data as Map);
final dynamic rawList = data['serviceList'];
if (rawList is! List) {
print(
'[WifiConfig] ERROR: serviceList is not a List or is missing: $rawList');
return <int>[];
}
final List<int> result = <int>[];
for (final dynamic rawItem in rawList) {
if (rawItem is! Map) continue;
final Map<String, dynamic> item = Map<String, dynamic>.from(rawItem);
final String host = item['serviceIp']?.toString().trim() ?? '';
final int? port = int.tryParse(item['port']?.toString() ?? '');
print('[WifiConfig] server item: host=$host, port=$port');
if (host.isEmpty || port == null || port < 1 || port > 65535) continue;
InternetAddress? address;
final InternetAddress? literal = InternetAddress.tryParse(host);
if (literal != null && literal.type == InternetAddressType.IPv4) {
address = literal;
} else {
try {
final List<InternetAddress> addresses =
await InternetAddress.lookup(host,
type: InternetAddressType.IPv4);
if (addresses.isNotEmpty) address = addresses.first;
} catch (e) {
print('[WifiConfig] DNS lookup failed for $host: $e');
}
}
if (address == null) continue;
result.addAll(address.rawAddress);
result.add(port ~/ 256);
result.add(port % 256);
print(
'[WifiConfig] added server: ${address.address}:$port -> ${address.rawAddress}, ${port ~/ 256}, ${port % 256}');
}
print('[WifiConfig] total servers fetched: ${result.length ~/ 6}');
return result;
} catch (e, s) {
print('[WifiConfig] ERROR in _fetchLockWifiServers: $e\n$s');
rethrow;
}
}
}

View File

@ -0,0 +1,97 @@
import 'package:starcloud/sdk/entity/cloud_user_info.dart';
import 'package:starcloud/net/interceptor/token_interceptor.dart';
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/starcloud.dart';
import '../core/callback_bridge.dart';
import '../core/starcloud_exception.dart';
class StarCloudAuthService {
StarCloudAuthService();
bool _initialized = false;
void init({
required String clientId,
required String clientSecret,
String? environmentUrl,
}) {
StarCloudSDK.init(
clientId: clientId,
clientSecret: clientSecret,
environmentUrl: environmentUrl,
);
_initialized = true;
}
void _ensureInitialized() {
if (!_initialized) {
throw const StarCloudException(
'StarCloud SDK is not initialized. Call auth.init() first.',
);
}
}
void setDebugMode({required bool debug}) {
_ensureInitialized();
StarCloudSDK.instance.setDebugMode(debug: debug);
}
Future<CloudUserInfo> createCloudUser() {
_ensureInitialized();
return CallbackBridge.fromCallback<CloudUserInfo>(
executor: (onSuccess, onError) {
StarCloudSDK.instance.createCloudUser(
onSuccess: onSuccess,
onError: onError,
);
},
);
}
void setCloudAccounts(List<CloudUserInfo> accounts) {
_ensureInitialized();
if (accounts.isEmpty) {
throw const StarCloudException('Cloud accounts cannot be empty.');
}
StarCloudSDK.instance.setCloudAccounts(accounts);
}
/// 登录 lock.skychip.top 授权账号并返回动态 accessToken。
Future<String> loginLockService({
required String username,
required String password,
String countryCode = '86',
String deviceBrand = 'android',
String deviceModel = 'unknown',
String deviceVersion = 'unknown',
}) async {
_ensureInitialized();
final String deviceId = await TokenInterceptors.getDeviceId();
final response = await SDKCommon.instance.netApi.userApi.loginLockService(
username: username,
password: password,
uniqueId: deviceId,
countryCode: countryCode,
deviceBrand: deviceBrand,
deviceModel: deviceModel,
deviceVersion: deviceVersion,
);
if (response.errcode != 0 || response.data is! Map) {
throw StarCloudException(
'Lock service login failed: ${response.errmsg}',
raw: response,
);
}
final String token =
(response.data as Map)['accessToken']?.toString().trim() ?? '';
if (token.isEmpty) {
throw StarCloudException(
'Lock service login response does not contain data.accessToken',
raw: response,
);
}
return token;
}
}

View File

@ -0,0 +1,59 @@
import 'dart:async';
import 'starcloud_exception.dart';
class CallbackBridge {
static Future<T> fromCallback<T>({
required void Function(
void Function(T value) onSuccess,
void Function(Object? error) onError,
) executor,
}) {
final completer = Completer<T>();
executor(
(value) {
if (!completer.isCompleted) completer.complete(value);
},
(error) {
if (!completer.isCompleted) {
completer.completeError(
StarCloudException(
error?.toString() ?? 'Unknown StarCloud error',
raw: error,
),
);
}
},
);
return completer.future;
}
static Future<void> fromVoidCallback({
required void Function(
void Function() onSuccess,
void Function(Object? error) onError,
) executor,
}) {
final completer = Completer<void>();
executor(
() {
if (!completer.isCompleted) completer.complete();
},
(error) {
if (!completer.isCompleted) {
completer.completeError(
StarCloudException(
error?.toString() ?? 'Unknown StarCloud error',
raw: error,
),
);
}
},
);
return completer.future;
}
}

View File

@ -0,0 +1,9 @@
class StarCloudException implements Exception {
final String message;
final Object? raw;
const StarCloudException(this.message, {this.raw});
@override
String toString() => 'StarCloudException(message: $message, raw: $raw)';
}

View File

@ -0,0 +1,80 @@
import 'package:starcloud/entity/star_cloud_lock_list.dart';
import 'package:starcloud/storage/storage_key.dart';
/// Encapsulates all credential and identity fields needed to execute
/// advanced BLE protocol commands for a specific lock.
///
/// Build once from a [StarCloudLock] (obtained via [StarCloudDeviceService.getDeviceList]
/// or [StarCloudDeviceService.pairDevice]), then pass to any `ByContext` method
/// on the advanced services — no need to manually supply keys, tokens, or IDs
/// on every call.
///
/// ```dart
/// // Token is fetched automatically from local storage (set during BLE auth).
/// final context = await StarCloudLockContext.fromLock(lock);
///
/// // Advanced API – only business parameters required
/// await StarCloudClient.instance.wifi.searchWifiByContext(context);
/// await StarCloudClient.instance.lockSetting.setRemoteUnlockByContext(
/// context,
/// enabled: true,
/// );
/// ```
class StarCloudLockContext {
const StarCloudLockContext({
required this.lockId,
required this.keyId,
required this.userId,
required this.token,
required this.publicKey,
required this.privateKey,
required this.signKey,
this.cloudUid,
this.needAuthor = 1,
});
/// Creates a [StarCloudLockContext] from a [StarCloudLock].
///
/// The BLE session token is read automatically from local storage — it is
/// written there by the underlying SDK after the device completes the
/// Bluetooth authentication handshake.
///
/// [needAuthor] defaults to `1` (authorization required). Pass `0` only when
/// the device explicitly does not require authorization.
static Future<StarCloudLockContext> fromLock(
StarCloudLock lock, {
int needAuthor = 1,
}) async {
var token = await StorageDeviceKey.getBlueToken(
lock.bluetooth.bluetoothDeviceName,
);
if (token.length < 4) {
token = List<int>.filled(4, 0);
}
return StarCloudLockContext(
lockId: lock.lockId,
keyId: lock.keyId.toString(),
userId: lock.uid.toString(),
token: token,
publicKey: lock.bluetooth.publicKey,
privateKey: lock.bluetooth.privateKey,
signKey: lock.bluetooth.signKey,
cloudUid: lock.cloudUid,
needAuthor: needAuthor,
);
}
final int lockId;
final String keyId;
final String userId;
final List<int> token;
final List<int> publicKey;
final List<int> privateKey;
final List<int> signKey;
/// 该锁所属的云账户 UID;与发给锁端的 [userId] 是不同身份域。
final int? cloudUid;
/// `1` = device requires authorization (default), `0` = no authorization.
final int needAuthor;
}

View File

@ -0,0 +1,91 @@
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/entity/entity_get/face_list.dart';
import 'package:starcloud/sdk/entity/entity_get/fingerprint_list.dart';
import 'package:starcloud/sdk/entity/entity_sender/face.dart';
import 'package:starcloud/sdk/entity/entity_sender/fingerprint.dart';
import '../core/callback_bridge.dart';
class StarCloudBiometricService {
StarCloudBiometricService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<FingerprintList> getFingerprintList({
required int lockId,
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<FingerprintList>(
executor: (onSuccess, onError) {
wrapper.getFingerprintList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> operateFingerprint({
required Fingerprint fingerprint,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.operateFingerprint(
fingerprint: fingerprint,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<FaceList> getFaceList({
required int lockId,
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<FaceList>(
executor: (onSuccess, onError) {
wrapper.getFaceList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> operateFace({
required Face face,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.operateFace(
face: face,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/entity/entity_get/card_list.dart';
import 'package:starcloud/sdk/entity/entity_sender/iccard.dart';
import '../core/callback_bridge.dart';
class StarCloudCardService {
StarCloudCardService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<CardList> getCardList({
required int lockId,
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<CardList>(
executor: (onSuccess, onError) {
wrapper.getCardList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> operateICCard({
required IcCard card,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.operateICCard(
card: card,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

View File

@ -0,0 +1,124 @@
import 'package:starcloud/net/entity/api_response.dart';
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/entity/entity_get/electric_key_list.dart';
import 'package:starcloud/sdk/entity/entity_sender/electric_key_model.dart';
import '../core/callback_bridge.dart';
class StarCloudElectricKeyService {
StarCloudElectricKeyService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<ElectricKeyList> getElectricKeysList({
required int lockId,
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ElectricKeyList>(
executor: (onSuccess, onError) {
wrapper.getElectricKeysList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> addElectricKeys({
required ElectricKeyModel electricKeyModel,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.addElectricKeys(
electricKeyModel: electricKeyModel,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> editElectricKeys({
required ElectricKeyItem electricKeyItem,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.editElectricKeys(
electricKeyItem: electricKeyItem,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> deleteElectricKeys({
required int keyId,
required int includeUnderlings,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.deletElectricKeys(
keyId: keyId,
includeUnderlings: includeUnderlings,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> freezeElectricKeys({
required int keyId,
required bool isFreeze,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.freezeElectricKeys(
keyId: keyId,
isFreeze: isFreeze,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> resetElectricKeys({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.resetElectricKeys(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

View File

@ -0,0 +1,94 @@
import 'package:starcloud/net/entity/api_response.dart';
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/entity/entity_get/password_list.dart';
import 'package:starcloud/sdk/entity/entity_sender/password.dart';
import '../core/callback_bridge.dart';
class StarCloudPasswordService {
StarCloudPasswordService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<PasswordList> getPasswordList({
required int lockId,
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<PasswordList>(
executor: (onSuccess, onError) {
wrapper.getPasswordList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> operateCustomPassword({
required PasswordModel passwordModel,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.operateCustomPassword(
passwordModel: passwordModel,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<ApiResponse?> operateOfflinePassword({
required PasswordModel passwordModel,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<ApiResponse?>(
executor: (onSuccess, onError) {
wrapper.operateOffLinePassword(
passwordModel: passwordModel,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<List<LockPassword>> batchGenerationOfflinePassword({
required List<PasswordModel> passwordModels,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return wrapper.batchGenerationOffLinePassword(passwordModels: passwordModels);
}
Future<void> resetPassword({
required PasswordModel passwordModel,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.resetPassword(
passwordModel: passwordModel,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

View File

@ -0,0 +1,174 @@
import 'package:starcloud/blue/blue_manage.dart';
import 'package:starcloud/blue/blue_read_write.dart';
import 'package:starcloud/entity/star_cloud_lock_list.dart';
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/starcloud.dart';
import '../core/callback_bridge.dart';
class StarCloudDeviceService {
StarCloudDeviceService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<List<StarCloudScanResult>> scanDeviceList({
int timeout = 3600,
bool notPaired = true,
void Function(List<StarCloudScanResult> devices)? onScan,
}) {
return CallbackBridge.fromCallback<List<StarCloudScanResult>>(
executor: (onSuccess, onError) {
StarCloudSDK.instance.scanDeviceList(
timeout: timeout,
notPaired: notPaired,
scanType: ScanType.lock,
onScan: (list) {
onScan?.call(list);
onSuccess(list);
},
);
},
);
}
Future<void> stopScan() {
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
StarCloudSDK.instance.stopScan(onError: onError).then((_) => onSuccess());
},
);
}
Future<void> disconnect() {
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
StarCloudSDK.instance.disconnect().then((_) => onSuccess());
},
);
}
Future<StarCloudLock> pairDevice({
required StarCloudScanResult scanResult,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<StarCloudLock>(
executor: (onSuccess, onError) {
wrapper.pairDevice(
scanResult: scanResult,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<StarCloudLockList> getDeviceList({
required int pageNo,
required int pageSize,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<StarCloudLockList>(
executor: (onSuccess, onError) {
wrapper.getDeviceList(
pageNo: pageNo,
pageSize: pageSize,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<StarCloudLock> getDevice({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<StarCloudLock>(
executor: (onSuccess, onError) {
wrapper.getDevice(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> openLock({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.openLock(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> closeLock({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.closeLock(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> resetLock({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.resetLock(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> updateAdminKeyboardPwd({
required int lockId,
required String pwd,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.updateAdminKeyboardPwd(
lockId: lockId,
pwd: pwd,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:starcloud/sdk/business/sdk_common.dart';
import 'package:starcloud/sdk/entity/entity_get/record_list.dart';
import '../core/callback_bridge.dart';
class StarCloudRecordService {
StarCloudRecordService();
Future<dynamic> _fetchWrapper(int? cloudUid) {
return SDKCommon.instance.fetchStarCloudUser(cloudUid);
}
Future<RecordList> getRecordList({
required int lockId,
required int pageNo,
required int pageSize,
required int startDate,
required int endDate,
required List<int> recordTypes,
int? cloudUid,
String? searchStr,
int? keyboardPwdId,
int? cardId,
int? fingerprintId,
int? keyId,
int? faceId,
int? remoteId,
int? palmVeinId,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromCallback<RecordList>(
executor: (onSuccess, onError) {
wrapper.getRecordList(
lockId: lockId,
pageNo: pageNo,
pageSize: pageSize,
startDate: startDate,
endDate: endDate,
recordTypes: recordTypes,
searchStr: searchStr,
keyboardPwdId: keyboardPwdId,
cardId: cardId,
fingerprintId: fingerprintId,
keyId: keyId,
faceId: faceId,
remoteId: remoteId,
palmVeinId: palmVeinId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
Future<void> uploadLockRecord({
required int lockId,
int? cloudUid,
}) async {
final wrapper = await _fetchWrapper(cloudUid);
return CallbackBridge.fromVoidCallback(
executor: (onSuccess, onError) {
wrapper.uploadLockRecord(
lockId: lockId,
onSuccess: onSuccess,
onError: onError,
);
},
);
}
}

49
lib/src/starcloud_client.dart Executable file
View File

@ -0,0 +1,49 @@
import 'advanced/protocol/starcloud_protocol_executor.dart';
import 'advanced/protocol/starcloud_protocol_executor_impl.dart';
import 'advanced/starcloud_lock_setting_service.dart';
import 'advanced/starcloud_lock_state_service.dart';
import 'advanced/starcloud_wifi_service.dart';
import 'auth/starcloud_auth_service.dart';
import 'credential/starcloud_biometric_service.dart';
import 'credential/starcloud_card_service.dart';
import 'credential/starcloud_electric_key_service.dart';
import 'credential/starcloud_password_service.dart';
import 'device/starcloud_device_service.dart';
import 'record/starcloud_record_service.dart';
import 'talk/starcloud_talk_service.dart';
class StarCloudClient {
StarCloudClient._() {
auth = StarCloudAuthService();
device = StarCloudDeviceService();
password = StarCloudPasswordService();
card = StarCloudCardService();
biometric = StarCloudBiometricService();
electricKey = StarCloudElectricKeyService();
record = StarCloudRecordService();
talk = StarCloudTalkService();
lockState = StarCloudLockStateService();
wifi = StarCloudWifiService();
lockSetting = StarCloudLockSettingService(StarCloudProtocolExecutorImpl());
}
static late StarCloudClient instance;
static void bootstrap() {
instance = StarCloudClient._();
}
late final StarCloudAuthService auth;
late final StarCloudDeviceService device;
late final StarCloudPasswordService password;
late final StarCloudCardService card;
late final StarCloudBiometricService biometric;
late final StarCloudElectricKeyService electricKey;
late final StarCloudRecordService record;
late final StarCloudTalkService talk;
late final StarCloudLockStateService lockState;
late final StarCloudWifiService wifi;
late final StarCloudLockSettingService lockSetting;
}

View File

@ -0,0 +1,271 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:starchart/starchart_flutter_sdk.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 视频对讲服务
///
/// 提供视频门铃的可视对讲功能,包括:
/// - 接收来电
/// - 接听/拒绝/挂断
/// - 接收视频帧(H264/MJPEG)和音频(G711)
/// - 发送音频
class StarCloudTalkService {
StarChartClient? _client;
bool _initialized = false;
Future<void>? _initFuture;
// 事件流控制器(用于暴露给外部)
final _incomingCallCtrl = StreamController<IncomingCallEvent>.broadcast();
final _talkDataCtrl = StreamController<TalkDataEvent>.broadcast();
final _talkStatusCtrl = StreamController<TalkStatusEvent>.broadcast();
/// 收到来电事件
Stream<IncomingCallEvent> get onIncomingCall => _incomingCallCtrl.stream;
/// 收到音视频数据事件
Stream<TalkDataEvent> get onTalkData => _talkDataCtrl.stream;
/// 通话状态变化事件
Stream<TalkStatusEvent> get onTalkStatus => _talkStatusCtrl.stream;
final _talkExpectRespCtrl = StreamController<TalkExpectRespEvent>.broadcast();
/// 收到锁端协商后的实际视频分辨率和编码格式
Stream<TalkExpectRespEvent> get onTalkExpectResp =>
_talkExpectRespCtrl.stream;
/// 当前通话状态
TalkStatus get status => _client?.talk.status ?? TalkStatus.idle;
/// 当前通话的锁 PeerID
String get currentLockPeerId => _client?.talk.currentLockPeerId ?? '';
/// 本机 PeerID(注册后生成,用于被锁板呼叫)
String get myPeerId => _client?.talk.fromPeerId ?? '';
void _ensureInitialized() {
if (!_initialized) {
throw const StarChartException(
'Talk service is not initialized. Call talk.init() first.',
);
}
}
/// 初始化视频对讲服务
///
/// [productName] 产品名称,如 "MyApp"
/// [unique] 设备唯一标识,如 deviceId
/// [baseUrl] 星图服务器地址(可选)
/// [localUdpPort] 本地 UDP 端口(可选)
Future<void> init({
required String productName,
required String unique,
String baseUrl = 'http://scd.skychip.top:8710',
int localUdpPort = 62289,
}) async {
if (_initialized) return;
final pending = _initFuture;
if (pending != null) {
await pending;
return;
}
final future = _doInit(
productName: productName,
unique: unique,
baseUrl: baseUrl,
localUdpPort: localUdpPort,
);
_initFuture = future;
try {
await future;
} finally {
if (identical(_initFuture, future)) {
_initFuture = null;
}
}
}
Future<void> _doInit({
required String productName,
required String unique,
required String baseUrl,
required int localUdpPort,
}) async {
final prefs = await SharedPreferences.getInstance();
final credKey = 'starchart_credential_$unique';
Future<StarChartClient> initializeClient(
PeerCredential? persistCred,
) async {
final client = StarChartClient();
_client = client;
// 必须在 client.init() 之前订阅事件。Relay/Talk 初始化期间可能已经
// 收到响应;若初始化完成后才监听,首个状态事件会被丢弃。
client.onIncomingCall.listen(_incomingCallCtrl.add);
client.onTalkData.listen(_talkDataCtrl.add);
client.onTalkStatus.listen(_talkStatusCtrl.add);
client.onTalkExpectResp.listen(_talkExpectRespCtrl.add);
try {
await client.init(
productName: productName,
unique: unique,
baseUrl: baseUrl,
localUdpPort: localUdpPort,
persistCredential: persistCred != null ? () => persistCred : null,
saveCredential: (cred) async {
final json = _encodeCredential(cred);
await prefs.setString(credKey, json);
},
);
return client;
} catch (_) {
client.dispose();
if (identical(_client, client)) _client = null;
rethrow;
}
}
PeerCredential? persistCred;
final credJson = prefs.getString(credKey);
if (credJson != null) {
try {
final map = _parseCredentialJson(credJson);
persistCred = PeerCredential(
id: map['id'] ?? '',
publicKey: map['publicKey'] ?? '',
privateKey: map['privateKey'] ?? '',
);
} catch (_) {}
}
try {
try {
await initializeClient(persistCred);
} catch (e) {
if (persistCred == null || !_isMissingPersistedPeer(e)) rethrow;
// 服务端可能清理了旧 Peer,但本地仍持有凭证。清除后重新注册一次,
// 避免每次地址上报都因 "record not found" 初始化失败。
await prefs.remove(credKey);
await initializeClient(null);
}
_initialized = true;
} catch (_) {
_client?.dispose();
_client = null;
rethrow;
}
}
bool _isMissingPersistedPeer(Object error) {
final message = error.toString().toLowerCase();
return message.contains('get peer error') &&
message.contains('record not found');
}
Map<String, String> _parseCredentialJson(String json) {
// 简单解析 JSON
final result = <String, String>{};
final regex = RegExp(r'"(\w+)":\s*"([^"]*)"');
for (final match in regex.allMatches(json)) {
result[match.group(1)!] = match.group(2)!;
}
return result;
}
String _encodeCredential(PeerCredential cred) {
return '{"id":"${cred.id}","publicKey":"${cred.publicKey}","privateKey":"${cred.privateKey}"}';
}
/// App 主动发起对讲请求
///
/// [lockPeerId] 目标锁的 PeerID
/// [callerName] 呼叫方名称
/// [options] 期望接收的音视频格式(可选,默认 H264 + G711)
void requestCall({
required String lockPeerId,
String callerName = 'App',
TalkOptions options = TalkOptions.defaultOptions,
}) {
_ensureInitialized();
_client!.talk.requestCall(
lockPeerId: lockPeerId,
callerName: callerName,
options: options,
);
}
/// 接听来电
///
/// [lockPeerId] 发起呼叫的锁 PeerID(从 IncomingCallEvent 获取)
/// [options] 期望接收的音视频格式(可选)
void accept({
required String lockPeerId,
TalkOptions options = TalkOptions.defaultOptions,
}) {
_ensureInitialized();
_client!.talk.accept(lockPeerId: lockPeerId, options: options);
}
/// 拒绝来电
void reject({required String lockPeerId}) {
_ensureInitialized();
_client!.talk.reject(lockPeerId: lockPeerId);
}
/// 挂断通话
void hangup({required String lockPeerId}) {
_ensureInitialized();
_client!.talk.hangup(lockPeerId: lockPeerId);
}
/// 发送 G711 音频数据
///
/// [g711Data] 已编码为 G711 ALaw 的音频字节
/// [durationMs] 音频时长(毫秒)
void sendAudio({required Uint8List g711Data, int durationMs = 0}) {
_ensureInitialized();
_client!.talk.sendAudio(g711Data: g711Data, durationMs: durationMs);
}
/// 发送 JPEG 视频帧
void sendJpegFrame({required Uint8List jpegData, int durationMs = 0}) {
_ensureInitialized();
_client!.talk.sendJpegFrame(jpegData: jpegData, durationMs: durationMs);
}
/// 通话中重新协商视频格式,并请求锁端重新输出关键帧。
void renegotiate({TalkOptions options = TalkOptions.defaultOptions}) {
_ensureInitialized();
_client!.talk.renegotiate(options: options);
}
/// 转移接听者到另一设备
void transferReceiver({
required String lockPeerId,
required String newPeerId,
}) {
_ensureInitialized();
_client!.talk.transferReceiver(
lockPeerId: lockPeerId,
newPeerId: newPeerId,
);
}
/// 释放资源
void dispose() {
_client?.dispose();
_client = null;
_initialized = false;
_initFuture = null;
_incomingCallCtrl.close();
_talkDataCtrl.close();
_talkStatusCtrl.close();
_talkExpectRespCtrl.close();
}
}

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