Merge branch 'develop_sky' into 'master_sky'
Develop sky See merge request StarlockTeam/app-starlock!111
This commit is contained in:
commit
c30dfdc1eb
@ -16,10 +16,10 @@ variables:
|
|||||||
- macos
|
- macos
|
||||||
- flutter
|
- flutter
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||||
- if: $CI_COMMIT_BRANCH == "release"
|
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||||
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
||||||
- if: $CI_COMMIT_BRANCH == "canary_release"
|
- if: $CI_COMMIT_BRANCH == "canary_release_sky"
|
||||||
- if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/
|
- if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/
|
||||||
|
|
||||||
.notify_rule:
|
.notify_rule:
|
||||||
@ -27,8 +27,8 @@ variables:
|
|||||||
- macos
|
- macos
|
||||||
- flutter
|
- flutter
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||||
- if: $CI_COMMIT_BRANCH == "release"
|
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||||
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
||||||
|
|
||||||
.generate_tag_rule:
|
.generate_tag_rule:
|
||||||
@ -36,16 +36,16 @@ variables:
|
|||||||
- macos
|
- macos
|
||||||
- flutter
|
- flutter
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "master"
|
- if: $CI_COMMIT_BRANCH == "master_sky"
|
||||||
|
|
||||||
.generate_next_version_rule:
|
.generate_next_version_rule:
|
||||||
tags:
|
tags:
|
||||||
- macos
|
- macos
|
||||||
- flutter
|
- flutter
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH == "develop"
|
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||||
- if: $CI_COMMIT_BRANCH == "release"
|
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||||
- if: $CI_COMMIT_BRANCH == "canary_release"
|
- if: $CI_COMMIT_BRANCH == "canary_release_sky"
|
||||||
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
||||||
|
|
||||||
.print_env:
|
.print_env:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# 星锁APP
|
# 星锁APP
|
||||||
|
测试ci
|
||||||
星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。
|
星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。
|
||||||
|
|
||||||
基于Flutter技术架构,支持Android和iOS平台。
|
基于Flutter技术架构,支持Android和iOS平台。
|
||||||
|
|||||||
@ -8,8 +8,9 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR}
|
|||||||
echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}"
|
echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}"
|
||||||
cd ${CI_PROJECT_DIR}/android
|
cd ${CI_PROJECT_DIR}/android
|
||||||
echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}"
|
echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}"
|
||||||
regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$'
|
# 只支持 v1.2.3_sky 这种tag格式
|
||||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then
|
regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$'
|
||||||
|
if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then
|
||||||
echo "===build canary_release: ${NEXT_VERSION}"
|
echo "===build canary_release: ${NEXT_VERSION}"
|
||||||
export ENV_BUILD_TAG=${NEXT_VERSION}
|
export ENV_BUILD_TAG=${NEXT_VERSION}
|
||||||
bundle exec fastlane release_apk flavor:xhj --verbose
|
bundle exec fastlane release_apk flavor:xhj --verbose
|
||||||
@ -20,11 +21,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then
|
|||||||
bundle exec fastlane release_apk flavor:sky --verbose
|
bundle exec fastlane release_apk flavor:sky --verbose
|
||||||
bundle exec fastlane release_bundle flavor:xhj_bundle --verbose
|
bundle exec fastlane release_bundle flavor:xhj_bundle --verbose
|
||||||
bundle exec fastlane release_bundle flavor:sky --verbose
|
bundle exec fastlane release_bundle flavor:sky --verbose
|
||||||
elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then
|
elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then
|
||||||
echo "===build dev===${NEXT_VERSION}"
|
echo "===build dev===${NEXT_VERSION}"
|
||||||
bundle exec fastlane beta flavor:xhj env:dev --verbose
|
bundle exec fastlane beta flavor:xhj env:dev --verbose
|
||||||
bundle exec fastlane beta flavor:sky env:dev --verbose
|
bundle exec fastlane beta flavor:sky env:dev --verbose
|
||||||
elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then
|
elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then
|
||||||
echo "===build pre===${NEXT_VERSION}"
|
echo "===build pre===${NEXT_VERSION}"
|
||||||
bundle exec fastlane beta flavor:xhj env:pre --verbose
|
bundle exec fastlane beta flavor:xhj env:pre --verbose
|
||||||
bundle exec fastlane beta flavor:sky env:pre --verbose
|
bundle exec fastlane beta flavor:sky env:pre --verbose
|
||||||
|
|||||||
@ -29,8 +29,8 @@
|
|||||||
|
|
||||||
#player {
|
#player {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
height: 56vh;
|
width: 100vw;
|
||||||
transform: rotate(-90deg);
|
height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,9 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}"
|
|||||||
cd ${CI_PROJECT_DIR}/ios
|
cd ${CI_PROJECT_DIR}/ios
|
||||||
#bundle exec pod install
|
#bundle exec pod install
|
||||||
echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}"
|
echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}"
|
||||||
regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$'
|
# 只支持 v1.2.3_sky 这种tag格式
|
||||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then
|
regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$'
|
||||||
|
if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then
|
||||||
echo "===build canary_release: ${NEXT_VERSION}"
|
echo "===build canary_release: ${NEXT_VERSION}"
|
||||||
export ENV_BUILD_TAG=${NEXT_VERSION}
|
export ENV_BUILD_TAG=${NEXT_VERSION}
|
||||||
bundle exec fastlane release_ipa flavor:xhj --verbose
|
bundle exec fastlane release_ipa flavor:xhj --verbose
|
||||||
@ -19,11 +20,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then
|
|||||||
echo "===build release===$ENV_BUILD_TAG"
|
echo "===build release===$ENV_BUILD_TAG"
|
||||||
bundle exec fastlane release_ipa flavor:xhj --verbose
|
bundle exec fastlane release_ipa flavor:xhj --verbose
|
||||||
bundle exec fastlane release_ipa flavor:sky --verbose
|
bundle exec fastlane release_ipa flavor:sky --verbose
|
||||||
elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then
|
elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then
|
||||||
echo "===build dev===${NEXT_VERSION}"
|
echo "===build dev===${NEXT_VERSION}"
|
||||||
bundle exec fastlane beta flavor:xhj env:Dev --verbose
|
bundle exec fastlane beta flavor:xhj env:Dev --verbose
|
||||||
bundle exec fastlane beta flavor:sky env:Dev --verbose
|
bundle exec fastlane beta flavor:sky env:Dev --verbose
|
||||||
elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then
|
elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then
|
||||||
echo "===build pre===${NEXT_VERSION}"
|
echo "===build pre===${NEXT_VERSION}"
|
||||||
bundle exec fastlane beta flavor:xhj env:Pre --verbose
|
bundle exec fastlane beta flavor:xhj env:Pre --verbose
|
||||||
bundle exec fastlane beta flavor:sky env:Pre --verbose
|
bundle exec fastlane beta flavor:sky env:Pre --verbose
|
||||||
|
|||||||
@ -60,6 +60,7 @@ import 'package:star_lock/mine/mineSet/transferSmartLock/transferSmartLockList/t
|
|||||||
import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_page.dart';
|
import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_page.dart';
|
||||||
import 'package:star_lock/mine/valueAddedServices/advancedFunctionRecord/advancedFunctionRecord_page.dart';
|
import 'package:star_lock/mine/valueAddedServices/advancedFunctionRecord/advancedFunctionRecord_page.dart';
|
||||||
import 'package:star_lock/mine/valueAddedServices/valueAddedServicesRecord/value_added_services_record_page.dart';
|
import 'package:star_lock/mine/valueAddedServices/valueAddedServicesRecord/value_added_services_record_page.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_page.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart';
|
||||||
import 'package:star_lock/talk/starChart/webView/h264_web_view.dart';
|
import 'package:star_lock/talk/starChart/webView/h264_web_view.dart';
|
||||||
|
|
||||||
@ -202,6 +203,7 @@ import 'mine/valueAddedServices/valueAddedServicesRealName/value_added_services_
|
|||||||
import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesAddSMSTemplate/newSMSTemplate_page.dart';
|
import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesAddSMSTemplate/newSMSTemplate_page.dart';
|
||||||
import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesListSMSTemplate/customSMSTemplateList_page.dart';
|
import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesListSMSTemplate/customSMSTemplateList_page.dart';
|
||||||
import 'starLockApplication/starLockApplication.dart';
|
import 'starLockApplication/starLockApplication.dart';
|
||||||
|
import 'talk/starChart/views/imageTransmission/image_transmission_page.dart';
|
||||||
import 'tools/seletKeyCyclicDate/seletKeyCyclicDate_page.dart';
|
import 'tools/seletKeyCyclicDate/seletKeyCyclicDate_page.dart';
|
||||||
|
|
||||||
abstract class Routers {
|
abstract class Routers {
|
||||||
@ -514,6 +516,8 @@ abstract class Routers {
|
|||||||
static const String starChartPage = '/starChartPage'; //星图
|
static const String starChartPage = '/starChartPage'; //星图
|
||||||
static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面
|
static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面
|
||||||
static const String h264WebView = '/h264WebView'; //星图对讲页面
|
static const String h264WebView = '/h264WebView'; //星图对讲页面
|
||||||
|
static const String imageTransmissionView =
|
||||||
|
'/imageTransmissionView'; //星图对讲页面(图传)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AppRouters {
|
abstract class AppRouters {
|
||||||
@ -1184,6 +1188,13 @@ abstract class AppRouters {
|
|||||||
page: () => const DoubleLockLinkPage()),
|
page: () => const DoubleLockLinkPage()),
|
||||||
GetPage<dynamic>(
|
GetPage<dynamic>(
|
||||||
name: Routers.starChartTalkView, page: () => const TalkViewPage()),
|
name: Routers.starChartTalkView, page: () => const TalkViewPage()),
|
||||||
GetPage<dynamic>(name: Routers.h264WebView, page: () => H264WebView()),
|
GetPage<dynamic>(
|
||||||
|
name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()),
|
||||||
|
// 插件播放页面
|
||||||
|
GetPage<dynamic>(
|
||||||
|
name: Routers.imageTransmissionView,
|
||||||
|
page: () => ImageTransmissionPage()),
|
||||||
|
// 插件播放页面
|
||||||
|
// GetPage<dynamic>(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/app_settings/app_settings.dart';
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
import 'package:star_lock/tools/commonDataManage.dart';
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
|
|
||||||
@ -190,10 +191,12 @@ class BlueManage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isMatch = _isMatch(scanResult
|
final isMatch = _isMatch(
|
||||||
.advertisementData.serviceUuids
|
scanResult.advertisementData.serviceUuids
|
||||||
.map((e) => e.uuid)
|
.map((e) => e.uuid)
|
||||||
.toList());
|
.toList(),
|
||||||
|
isSingle: true,
|
||||||
|
);
|
||||||
|
|
||||||
if (isMatch && (scanResult.rssi >= -100)) {
|
if (isMatch && (scanResult.rssi >= -100)) {
|
||||||
// 查询id相同的元素
|
// 查询id相同的元素
|
||||||
@ -276,6 +279,7 @@ class BlueManage {
|
|||||||
.map((e) => e.uuid)
|
.map((e) => e.uuid)
|
||||||
.toList(),
|
.toList(),
|
||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
|
isSingle: false,
|
||||||
);
|
);
|
||||||
// 判断名字为空的直接剔除
|
// 判断名字为空的直接剔除
|
||||||
if (isMatch && (scanResult.rssi >= -100)) {
|
if (isMatch && (scanResult.rssi >= -100)) {
|
||||||
@ -320,26 +324,47 @@ class BlueManage {
|
|||||||
|
|
||||||
/// 判断是否包含指定的uuid
|
/// 判断是否包含指定的uuid
|
||||||
bool _isMatch(List<String> serviceUuids,
|
bool _isMatch(List<String> serviceUuids,
|
||||||
{DeviceType deviceType = DeviceType.blue}) {
|
{DeviceType deviceType = DeviceType.blue, required bool isSingle}) {
|
||||||
final List<String> prefixes =
|
final List<String> prefixes =
|
||||||
getDeviceType(deviceType).map((e) => e.toLowerCase()).toList();
|
getDeviceType(deviceType).map((e) => e.toLowerCase()).toList();
|
||||||
for (String uuid in serviceUuids) {
|
for (String uuid in serviceUuids) {
|
||||||
final String cleanUuid = uuid.replaceAll('-', '').toLowerCase();
|
final String cleanUuid = uuid.toLowerCase();
|
||||||
if (cleanUuid.length == 8) {
|
if (cleanUuid.length == 8) {
|
||||||
// 8位,判断前两位
|
// 8位,判断第4、5位
|
||||||
|
String pairStatus = cleanUuid.substring(4, 6); // 第4、5位(索引3和4)
|
||||||
for (final prefix in prefixes) {
|
for (final prefix in prefixes) {
|
||||||
if (cleanUuid.startsWith(prefix)) {
|
if (cleanUuid.startsWith(prefix)) {
|
||||||
return true;
|
if (isSingle) {
|
||||||
|
return true; // isSingle为true,前缀匹配即返回true
|
||||||
|
} else {
|
||||||
|
// 00=未配对,01=已配对
|
||||||
|
if (pairStatus == '00') {
|
||||||
|
return true; // 未配对才返回true
|
||||||
|
}
|
||||||
|
// 已配对(01)不返回true,继续判断下一个uuid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (cleanUuid.length == 32) {
|
} else {
|
||||||
// 128位,判断前8位的第3、第4位
|
// 128位,判断前8位的第3、第4位
|
||||||
final String first8 = cleanUuid.substring(0, 8);
|
if (cleanUuid.length >= 32) {
|
||||||
if (first8.length >= 4) {
|
final String thirdAndFourth = cleanUuid.substring(2, 4); // 索引2和3
|
||||||
final String thirdAndFourth = first8.substring(2, 4); // 索引2和3
|
|
||||||
for (final prefix in prefixes) {
|
for (final prefix in prefixes) {
|
||||||
if (thirdAndFourth == prefix) {
|
if (thirdAndFourth == prefix) {
|
||||||
return true;
|
if (isSingle) {
|
||||||
|
return true; // isSingle为true,前缀匹配即返回true
|
||||||
|
} else {
|
||||||
|
// 判断配对状态(带横杠UUID的第31、32位,从1开始计数)
|
||||||
|
if (cleanUuid.length >= 32) {
|
||||||
|
String pairStatus =
|
||||||
|
cleanUuid.substring(30, 32); // 第31、32位(从1开始计数)
|
||||||
|
// 00=未配对,01=已配对
|
||||||
|
if (pairStatus == '00') {
|
||||||
|
return true; // 未配对才返回true
|
||||||
|
}
|
||||||
|
// 已配对(01)不返回true,继续判断下一个uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -567,7 +592,10 @@ class BlueManage {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
connectStateCallBack(BluetoothConnectionState.disconnected);
|
connectStateCallBack(BluetoothConnectionState.disconnected);
|
||||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
if (!F.isSKY) {
|
||||||
|
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
scanDevices.clear();
|
scanDevices.clear();
|
||||||
|
|
||||||
BuglyTool.uploadException(
|
BuglyTool.uploadException(
|
||||||
@ -598,7 +626,9 @@ class BlueManage {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
connectStateCallBack(BluetoothConnectionState.disconnected);
|
connectStateCallBack(BluetoothConnectionState.disconnected);
|
||||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
if (!F.isSKY) {
|
||||||
|
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||||
|
}
|
||||||
scanDevices.clear();
|
scanDevices.clear();
|
||||||
|
|
||||||
BuglyTool.uploadException(
|
BuglyTool.uploadException(
|
||||||
@ -791,7 +821,7 @@ class BlueManage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入
|
/// 写入蓝牙特征值,并等待响应
|
||||||
Future<void> writeCharacteristicWithResponse(List<int> value) async {
|
Future<void> writeCharacteristicWithResponse(List<int> value) async {
|
||||||
final List<BluetoothService> services =
|
final List<BluetoothService> services =
|
||||||
await bluetoothConnectDevice!.discoverServices();
|
await bluetoothConnectDevice!.discoverServices();
|
||||||
@ -801,30 +831,70 @@ class BlueManage {
|
|||||||
in service.characteristics) {
|
in service.characteristics) {
|
||||||
if (characteristic.characteristicUuid == _characteristicIdWrite) {
|
if (characteristic.characteristicUuid == _characteristicIdWrite) {
|
||||||
try {
|
try {
|
||||||
|
// 添加重试机制
|
||||||
|
int retryCount = 0;
|
||||||
|
const int maxRetries = 3;
|
||||||
|
const int retryDelayMs = 500;
|
||||||
|
|
||||||
final List<int> valueList = value;
|
final List<int> valueList = value;
|
||||||
final List subData = splitList(valueList, _mtuSize!);
|
final List subData = splitList(valueList, _mtuSize!);
|
||||||
// AppLog.log('writeCharacteristicWithResponse _mtuSize:$_mtuSize 得到的分割数据:$subData');
|
|
||||||
for (int i = 0; i < subData.length; i++) {
|
for (int i = 0; i < subData.length; i++) {
|
||||||
if (characteristic.properties.writeWithoutResponse) {
|
// 对每个数据包都应用重试逻辑
|
||||||
// 使用WRITE_NO_RESPONSE属性写入值
|
bool packetSent = false;
|
||||||
await characteristic.write(subData[i], withoutResponse: true);
|
retryCount = 0;
|
||||||
} else if (characteristic.properties.write) {
|
|
||||||
// 使用WRITE属性写入值
|
while (!packetSent && retryCount < maxRetries) {
|
||||||
await characteristic.write(subData[i]);
|
try {
|
||||||
} else {
|
if (characteristic.properties.writeWithoutResponse) {
|
||||||
// 特性不支持写入
|
await characteristic.write(subData[i],
|
||||||
|
withoutResponse: true);
|
||||||
|
} else if (characteristic.properties.write) {
|
||||||
|
await characteristic.write(subData[i]);
|
||||||
|
} else {
|
||||||
|
// 特性不支持写入
|
||||||
|
throw Exception(
|
||||||
|
'This characteristic does not support writing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果到这里没有异常,则包发送成功
|
||||||
|
packetSent = true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') &&
|
||||||
|
retryCount < maxRetries - 1) {
|
||||||
|
// GATT错误133,尝试重试
|
||||||
|
retryCount++;
|
||||||
|
AppLog.log(
|
||||||
|
'蓝牙写入失败(GATT 133),数据包 ${i + 1}/${subData.length} 正在重试 $retryCount/$maxRetries...');
|
||||||
|
await Future.delayed(
|
||||||
|
Duration(milliseconds: retryDelayMs));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// 其他错误或已达到最大重试次数,抛出异常
|
||||||
|
AppLog.log('APP写入失败: $e');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packetSent) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'This characteristic does not support writing.');
|
'蓝牙写入失败,数据包 ${i + 1}/${subData.length} 已达到最大重试次数');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return; // 所有数据包都发送成功
|
||||||
} on Exception catch (e, s) {
|
} on Exception catch (e, s) {
|
||||||
AppLog.log('APP写入失败: $e $s');
|
AppLog.log('APP写入失败: $e $s');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果找不到合适的特性用于写入
|
||||||
|
throw Exception('未找到适合写入的蓝牙特性');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止扫描蓝牙设备
|
// 停止扫描蓝牙设备
|
||||||
|
|||||||
@ -61,16 +61,16 @@ class CommandReciverManager {
|
|||||||
final int dataSize = data.length;
|
final int dataSize = data.length;
|
||||||
|
|
||||||
// 验证CRC校验
|
// 验证CRC校验
|
||||||
if (dataSize >= 2) {
|
// if (dataSize >= 2) {
|
||||||
final int calculatedCrc =
|
// final int calculatedCrc =
|
||||||
_calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2);
|
// _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2);
|
||||||
final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1];
|
// final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1];
|
||||||
|
//
|
||||||
if (calculatedCrc != receivedCrc) {
|
// if (calculatedCrc != receivedCrc) {
|
||||||
AppLog.log('CRC校验失败');
|
// throw Exception('CRC校验失败');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// 当小于包头加起来13个字节
|
// 当小于包头加起来13个字节
|
||||||
if (dataSize < 13) {
|
if (dataSize < 13) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart';
|
import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart';
|
||||||
|
|
||||||
import '../../../../appRouters.dart';
|
import '../../../../appRouters.dart';
|
||||||
@ -179,13 +180,22 @@ class _CardDetailPageState extends State<CardDetailPage> with RouteAware {
|
|||||||
isHaveRightWidget: true,
|
isHaveRightWidget: true,
|
||||||
rightWidget: SizedBox(
|
rightWidget: SizedBox(
|
||||||
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
||||||
Obx(() => CommonItem(
|
Visibility(
|
||||||
leftTitel: '是否为管理员'.tr,
|
visible: !F.isSKY,
|
||||||
rightTitle: '',
|
child: Obx(
|
||||||
isTipsImg: false,
|
() => CommonItem(
|
||||||
isHaveRightWidget: true,
|
leftTitel: '是否为管理员'.tr,
|
||||||
rightWidget:
|
rightTitle: '',
|
||||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
isTipsImg: false,
|
||||||
|
isHaveRightWidget: true,
|
||||||
|
rightWidget: SizedBox(
|
||||||
|
width: 60.w,
|
||||||
|
height: 50.h,
|
||||||
|
child: _isAdmin(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Container(height: 10.h),
|
Container(height: 10.h),
|
||||||
CommonItem(
|
CommonItem(
|
||||||
leftTitel: '操作记录'.tr,
|
leftTitel: '操作记录'.tr,
|
||||||
|
|||||||
@ -249,8 +249,9 @@ class CardListLogic extends BaseGetXController {
|
|||||||
_initReplySubscription();
|
_initReplySubscription();
|
||||||
|
|
||||||
// _initRefreshAction();
|
// _initRefreshAction();
|
||||||
|
await getICCardListData(isRefresh: true);
|
||||||
}
|
}
|
||||||
await getICCardListData(isRefresh: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_logic.dart';
|
import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_logic.dart';
|
||||||
import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_state.dart';
|
import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_state.dart';
|
||||||
|
|
||||||
@ -174,13 +175,22 @@ class _FaceDetailPageState extends State<FaceDetailPage> with RouteAware {
|
|||||||
// isHaveRightWidget: true,
|
// isHaveRightWidget: true,
|
||||||
// isHaveLine: true,
|
// isHaveLine: true,
|
||||||
// rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))),
|
// rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))),
|
||||||
Obx(() => CommonItem(
|
Visibility(
|
||||||
leftTitel: '是否为管理员'.tr,
|
visible: !F.isSKY,
|
||||||
rightTitle: '',
|
child: Obx(
|
||||||
isTipsImg: false,
|
() => CommonItem(
|
||||||
isHaveRightWidget: true,
|
leftTitel: '是否为管理员'.tr,
|
||||||
rightWidget:
|
rightTitle: '',
|
||||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
isTipsImg: false,
|
||||||
|
isHaveRightWidget: true,
|
||||||
|
rightWidget: SizedBox(
|
||||||
|
width: 60.w,
|
||||||
|
height: 50.h,
|
||||||
|
child: _isAdmin(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Container(height: 10.h),
|
Container(height: 10.h),
|
||||||
CommonItem(
|
CommonItem(
|
||||||
leftTitel: '操作记录'.tr,
|
leftTitel: '操作记录'.tr,
|
||||||
|
|||||||
@ -443,8 +443,9 @@ class FaceListLogic extends BaseGetXController {
|
|||||||
// senderCheckingCardStatus();
|
// senderCheckingCardStatus();
|
||||||
|
|
||||||
// senderCheckingUserInfoCount();
|
// senderCheckingUserInfoCount();
|
||||||
|
await getFaceListData(isRefresh: true);
|
||||||
}
|
}
|
||||||
getFaceListData(isRefresh: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/apm/apm_helper.dart';
|
import 'package:star_lock/apm/apm_helper.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||||||
import 'package:star_lock/main/lockDetail/fingerprint/addFingerprint/addFingerprint_entity.dart';
|
import 'package:star_lock/main/lockDetail/fingerprint/addFingerprint/addFingerprint_entity.dart';
|
||||||
import 'package:star_lock/tools/dateTool.dart';
|
import 'package:star_lock/tools/dateTool.dart';
|
||||||
@ -141,7 +142,7 @@ class AddFingerprintLogic extends BaseGetXController {
|
|||||||
break;
|
break;
|
||||||
case 0xFE:
|
case 0xFE:
|
||||||
case 12:
|
case 12:
|
||||||
// 管理员已满
|
// 管理员已满
|
||||||
state.ifAddState.value = false;
|
state.ifAddState.value = false;
|
||||||
showToast('管理员已满'.tr, something: () {
|
showToast('管理员已满'.tr, something: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
@ -250,6 +251,12 @@ class AddFingerprintLogic extends BaseGetXController {
|
|||||||
|
|
||||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||||
|
String startTime = DateTool().dateToHNString(state.effectiveDateTime.value);
|
||||||
|
String endTime = DateTool().dateToHNString(state.failureDateTime.value);
|
||||||
|
if (F.isSKY) {
|
||||||
|
startTime = '255:00';
|
||||||
|
endTime = '255:00';
|
||||||
|
}
|
||||||
|
|
||||||
final String command = SenderAddFingerprintWithTimeCycleCoercionCommand(
|
final String command = SenderAddFingerprintWithTimeCycleCoercionCommand(
|
||||||
keyID: '1',
|
keyID: '1',
|
||||||
@ -267,8 +274,8 @@ class AddFingerprintLogic extends BaseGetXController {
|
|||||||
// 周循环
|
// 周循环
|
||||||
startDate: int.parse(state.startDate.value) ~/ 1000,
|
startDate: int.parse(state.startDate.value) ~/ 1000,
|
||||||
endDate: int.parse(state.endDate.value) ~/ 1000,
|
endDate: int.parse(state.endDate.value) ~/ 1000,
|
||||||
startTime: DateTool().dateToHNString(state.effectiveDateTime.value),
|
startTime: startTime,
|
||||||
endTime: DateTool().dateToHNString(state.failureDateTime.value),
|
endTime: endTime,
|
||||||
needAuthor: 1,
|
needAuthor: 1,
|
||||||
signKey: signKeyDataList,
|
signKey: signKeyDataList,
|
||||||
privateKey: getPrivateKeyList,
|
privateKey: getPrivateKeyList,
|
||||||
@ -324,8 +331,8 @@ class AddFingerprintLogic extends BaseGetXController {
|
|||||||
// 周循环
|
// 周循环
|
||||||
startDate: int.parse(state.startDate.value) ~/ 1000,
|
startDate: int.parse(state.startDate.value) ~/ 1000,
|
||||||
endDate: int.parse(state.endDate.value) ~/ 1000,
|
endDate: int.parse(state.endDate.value) ~/ 1000,
|
||||||
startTime: DateTool().dateToHNString(state.effectiveDateTime.value),
|
startTime: startTime,
|
||||||
endTime: DateTool().dateToHNString(state.failureDateTime.value),
|
endTime: endTime,
|
||||||
needAuthor: 1,
|
needAuthor: 1,
|
||||||
signKey: signKeyDataList,
|
signKey: signKeyDataList,
|
||||||
privateKey: getPrivateKeyList,
|
privateKey: getPrivateKeyList,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart';
|
import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart';
|
||||||
|
|
||||||
import '../../../../appRouters.dart';
|
import '../../../../appRouters.dart';
|
||||||
@ -178,13 +179,22 @@ class _FingerprintDetailPageState extends State<FingerprintDetailPage>
|
|||||||
isHaveLine: true,
|
isHaveLine: true,
|
||||||
rightWidget: SizedBox(
|
rightWidget: SizedBox(
|
||||||
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
||||||
Obx(() => CommonItem(
|
Visibility(
|
||||||
leftTitel: '是否为管理员'.tr,
|
visible: !F.isSKY,
|
||||||
rightTitle: '',
|
child: Obx(
|
||||||
isTipsImg: false,
|
() => CommonItem(
|
||||||
isHaveRightWidget: true,
|
leftTitel: '是否为管理员'.tr,
|
||||||
rightWidget:
|
rightTitle: '',
|
||||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
isTipsImg: false,
|
||||||
|
isHaveRightWidget: true,
|
||||||
|
rightWidget: SizedBox(
|
||||||
|
width: 60.w,
|
||||||
|
height: 50.h,
|
||||||
|
child: _isAdmin(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Container(height: 10.h),
|
Container(height: 10.h),
|
||||||
CommonItem(
|
CommonItem(
|
||||||
leftTitel: '操作记录'.tr,
|
leftTitel: '操作记录'.tr,
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.dart';
|
|||||||
import 'package:star_lock/main/lockDetail/lockSet/lockTime/getServerDatetime_entity.dart';
|
import 'package:star_lock/main/lockDetail/lockSet/lockTime/getServerDatetime_entity.dart';
|
||||||
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
import 'package:star_lock/tools/throttler.dart';
|
import 'package:star_lock/tools/throttler.dart';
|
||||||
@ -572,7 +574,7 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
||||||
Future<void> getLockNetToken() async {
|
Future<void> getLockNetToken() async {
|
||||||
final LockNetTokenEntity entity = await ApiRepository.to
|
final LockNetTokenEntity entity = await ApiRepository.to
|
||||||
.getLockNetToken(lockId: state.keyInfos.value.lockId.toString());
|
.getLockNetToken(lockId: state.keyInfos.value.lockId!);
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
state.lockNetToken = entity.data!.token!.toString();
|
state.lockNetToken = entity.data!.token!.toString();
|
||||||
// AppLog.log('从服务器获取联网token:${state.lockNetToken}');
|
// AppLog.log('从服务器获取联网token:${state.lockNetToken}');
|
||||||
@ -752,42 +754,40 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
eventBus.fire(RefreshLockDetailInfoDataEvent());
|
eventBus.fire(RefreshLockDetailInfoDataEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求设备网络信息并设置
|
// /// 请求设备网络信息并设置
|
||||||
void requestDeviceNetworkInfo() async {
|
// void requestDeviceNetworkInfo() async {
|
||||||
final DeviceNetwork deviceNetworkInfo =
|
// final DeviceNetwork deviceNetworkInfo =
|
||||||
await ApiRepository.to.getDeviceNetwork(
|
// await ApiRepository.to.getDeviceNetwork(
|
||||||
deviceType: 2,
|
// deviceType: 2,
|
||||||
deviceMac: state.keyInfos.value.mac!,
|
// deviceMac: state.keyInfos.value.mac!,
|
||||||
);
|
// );
|
||||||
if (deviceNetworkInfo.data?.wifiName == null ||
|
// if (deviceNetworkInfo.data?.peerId == null ||
|
||||||
deviceNetworkInfo.data?.wifiName == '') {
|
// deviceNetworkInfo.data?.peerId == '') {
|
||||||
return;
|
// return;
|
||||||
} else {
|
// }
|
||||||
final peerId = deviceNetworkInfo?.data?.peerId;
|
// final peerId = deviceNetworkInfo!.data!.peerId;
|
||||||
if (peerId == null || peerId.isEmpty || peerId == '') {
|
// // 设置锁的peerID
|
||||||
throw Exception('设备peerId为空');
|
// StartChartManage().lockNetworkInfo =
|
||||||
}
|
// deviceNetworkInfo.data ?? DeviceNetworkInfo();
|
||||||
// 设置锁的peerID
|
// StartChartManage().lockPeerId = peerId!;
|
||||||
StartChartManage().lockNetworkInfo =
|
// }
|
||||||
deviceNetworkInfo.data ?? DeviceNetworkInfo();
|
|
||||||
StartChartManage().lockPeerId = peerId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 发送监控消息
|
/// 发送监控消息
|
||||||
void sendMonitorMessage() {
|
void sendMonitorMessage() {
|
||||||
final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? [];
|
final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? [];
|
||||||
|
final network = state.keyInfos.value.network;
|
||||||
if (catEyeConfig.isNotEmpty &&
|
if (catEyeConfig.isNotEmpty &&
|
||||||
catEyeConfig.length > 0 &&
|
catEyeConfig.length > 0 &&
|
||||||
catEyeConfig[0].catEyeMode != 0) {
|
catEyeConfig[0].catEyeMode != 0) {
|
||||||
if (StartChartManage().lockNetworkInfo.wifiName == null ||
|
if (network == null || network?.peerId == null || network?.peerId == '') {
|
||||||
StartChartManage().lockNetworkInfo.wifiName == '') {
|
|
||||||
showToast('设备未配网'.tr);
|
showToast('设备未配网'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 重置丢包率监控
|
||||||
|
// PacketLossStatistics().reset();
|
||||||
// 发送监控id
|
// 发送监控id
|
||||||
StartChartManage().startCallRequestMessageTimer(
|
StartChartManage()
|
||||||
ToPeerId: StartChartManage().lockPeerId ?? '');
|
.startCallRequestMessageTimer(ToPeerId: network!.peerId ?? '');
|
||||||
} else {
|
} else {
|
||||||
showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr);
|
showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr);
|
||||||
}
|
}
|
||||||
@ -799,12 +799,13 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
getServerDatetime();
|
getServerDatetime();
|
||||||
await PermissionDialog.request(Permission.location);
|
await PermissionDialog.request(Permission.location);
|
||||||
await PermissionDialog.requestBluetooth();
|
await PermissionDialog.requestBluetooth();
|
||||||
requestDeviceNetworkInfo();
|
// requestDeviceNetworkInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus
|
state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus
|
||||||
.on<LockSetChangeSetRefreshLockDetailWithType>()
|
.on<LockSetChangeSetRefreshLockDetailWithType>()
|
||||||
.listen((LockSetChangeSetRefreshLockDetailWithType event) {
|
.listen((LockSetChangeSetRefreshLockDetailWithType event) {
|
||||||
@ -851,11 +852,11 @@ class LockDetailLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.SuccessfulDistributionNetworkEvent = eventBus
|
// state.SuccessfulDistributionNetworkEvent = eventBus
|
||||||
.on<SuccessfulDistributionNetwork>()
|
// .on<SuccessfulDistributionNetwork>()
|
||||||
.listen((SuccessfulDistributionNetwork event) {
|
// .listen((SuccessfulDistributionNetwork event) {
|
||||||
// 配网成功获取一下配网信息
|
// // 配网成功获取一下配网信息
|
||||||
requestDeviceNetworkInfo();
|
// requestDeviceNetworkInfo();
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
@ -88,7 +89,6 @@ class _LockDetailPageState extends State<LockDetailPage>
|
|||||||
/// 路由订阅
|
/// 路由订阅
|
||||||
AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
|
AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||||
state.isOpenLockNeedOnline.refresh();
|
state.isOpenLockNeedOnline.refresh();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription? _lockRefreshLockDetailInfoDataEvent;
|
StreamSubscription? _lockRefreshLockDetailInfoDataEvent;
|
||||||
@ -1103,13 +1103,15 @@ class _LockDetailPageState extends State<LockDetailPage>
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// 密码
|
// 密码
|
||||||
showWidgetArr.add(bottomItem('images/main/icon_main_password.png', '密码'.tr,
|
if (state.keyInfos.value.lockFeature!.password == 1) {
|
||||||
state.bottomBtnisEable.value, () {
|
showWidgetArr.add(bottomItem('images/main/icon_main_password.png',
|
||||||
Get.toNamed(Routers.passwordKeyListPage,
|
'密码'.tr, state.bottomBtnisEable.value, () {
|
||||||
arguments: <String, LockListInfoItemEntity>{
|
Get.toNamed(Routers.passwordKeyListPage,
|
||||||
'keyInfo': state.keyInfos.value
|
arguments: <String, LockListInfoItemEntity>{
|
||||||
});
|
'keyInfo': state.keyInfos.value
|
||||||
}));
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// ic卡
|
// ic卡
|
||||||
if (state.keyInfos.value.lockFeature!.icCard == 1) {
|
if (state.keyInfos.value.lockFeature!.icCard == 1) {
|
||||||
@ -1179,7 +1181,7 @@ class _LockDetailPageState extends State<LockDetailPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
//可视对讲门锁新增->监控
|
//可视对讲门锁新增->监控
|
||||||
if (state.keyInfos.value.lockFeature!.videoIntercom == 1) {
|
if (state.keyInfos.value.lockFeature!.isSupportCatEye == 1) {
|
||||||
showWidgetArr.add(
|
showWidgetArr.add(
|
||||||
bottomItem('images/main/icon_catEyes.png', '监控'.tr,
|
bottomItem('images/main/icon_catEyes.png', '监控'.tr,
|
||||||
state.bottomBtnisEable.value, () async {
|
state.bottomBtnisEable.value, () async {
|
||||||
@ -1467,7 +1469,7 @@ class _LockDetailPageState extends State<LockDetailPage>
|
|||||||
state.iSOpenLock.value = true;
|
state.iSOpenLock.value = true;
|
||||||
state.openLockBtnState.value = 1;
|
state.openLockBtnState.value = 1;
|
||||||
state.animationController!.forward();
|
state.animationController!.forward();
|
||||||
// AppLog.log('点击开锁');
|
AppLog.log('点击开锁');
|
||||||
if (isOpenLockNeedOnline) {
|
if (isOpenLockNeedOnline) {
|
||||||
// 不需要联网
|
// 不需要联网
|
||||||
state.openDoorModel = 0;
|
state.openDoorModel = 0;
|
||||||
|
|||||||
@ -7,18 +7,18 @@ import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dar
|
|||||||
import '../../../blue/io_reply.dart';
|
import '../../../blue/io_reply.dart';
|
||||||
import '../../lockMian/entity/lockListInfo_entity.dart';
|
import '../../lockMian/entity/lockListInfo_entity.dart';
|
||||||
|
|
||||||
|
|
||||||
class LockDetailState {
|
class LockDetailState {
|
||||||
Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
|
Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
|
||||||
final Rx<LockSetInfoData> lockSetInfoData = LockSetInfoData().obs;
|
final Rx<LockSetInfoData> lockSetInfoData = LockSetInfoData().obs;
|
||||||
late StreamSubscription<Reply> replySubscription;
|
late StreamSubscription<Reply> replySubscription;
|
||||||
StreamSubscription? lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
StreamSubscription?
|
||||||
|
lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
||||||
StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription;
|
StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription;
|
||||||
StreamSubscription? DetailLockInfo;
|
StreamSubscription? DetailLockInfo;
|
||||||
StreamSubscription? SuccessfulDistributionNetworkEvent;
|
StreamSubscription? SuccessfulDistributionNetworkEvent;
|
||||||
|
|
||||||
String lockNetToken = '0';
|
String lockNetToken = '0';
|
||||||
int differentialTime = 0;// 服务器时间与本地时间差值
|
int differentialTime = 0; // 服务器时间与本地时间差值
|
||||||
bool isHaveNetwork = true;
|
bool isHaveNetwork = true;
|
||||||
int lockUserNo = 0;
|
int lockUserNo = 0;
|
||||||
int senderUserId = 0;
|
int senderUserId = 0;
|
||||||
@ -41,7 +41,7 @@ class LockDetailState {
|
|||||||
RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用
|
RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用
|
||||||
RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用
|
RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用
|
||||||
|
|
||||||
int openDoorModel = 0;// 离线开门0, 在线开门2 离线关门32 在线关门34
|
int openDoorModel = 0; // 离线开门0, 在线开门2 离线关门32 在线关门34
|
||||||
|
|
||||||
//过渡动画控制器
|
//过渡动画控制器
|
||||||
AnimationController? animationController;
|
AnimationController? animationController;
|
||||||
|
|||||||
@ -158,23 +158,23 @@ class _BasicInformationPageState extends State<BasicInformationPage> {
|
|||||||
allHeight: 70.h,
|
allHeight: 70.h,
|
||||||
isHaveLine: true),
|
isHaveLine: true),
|
||||||
)),
|
)),
|
||||||
Obx(() => CommonItem(
|
// Obx(() => CommonItem(
|
||||||
leftTitel: '位置信息'.tr,
|
// leftTitel: '位置信息'.tr,
|
||||||
// rightTitle: state.lockBasicInfo.value.address ?? "-",
|
// // rightTitle: state.lockBasicInfo.value.address ?? "-",
|
||||||
allHeight: 80.h,
|
// allHeight: 80.h,
|
||||||
isHaveLine: false,
|
// isHaveLine: false,
|
||||||
isHaveRightWidget: true,
|
// isHaveRightWidget: true,
|
||||||
rightWidget: SizedBox(
|
// rightWidget: SizedBox(
|
||||||
width: 300.w,
|
// width: 300.w,
|
||||||
child: Text(state.lockBasicInfo.value.address ?? '无'.tr,
|
// child: Text(state.lockBasicInfo.value.address ?? '无'.tr,
|
||||||
maxLines: 2,
|
// maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
// overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.end,
|
// textAlign: TextAlign.end,
|
||||||
style: TextStyle(
|
// style: TextStyle(
|
||||||
fontSize: 22.sp,
|
// fontSize: 22.sp,
|
||||||
color: AppColors.darkGrayTextColor)),
|
// color: AppColors.darkGrayTextColor)),
|
||||||
),
|
// ),
|
||||||
)),
|
// )),
|
||||||
/* 2024-01-12 会议确定去掉“微信二维码” by DaisyWu
|
/* 2024-01-12 会议确定去掉“微信二维码” by DaisyWu
|
||||||
CommonItem(
|
CommonItem(
|
||||||
leftTitel:
|
leftTitel:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
@ -17,31 +16,36 @@ import '../../../../tools/eventBusEventManage.dart';
|
|||||||
import '../../../../tools/storage.dart';
|
import '../../../../tools/storage.dart';
|
||||||
import 'burglarAlarm_state.dart';
|
import 'burglarAlarm_state.dart';
|
||||||
|
|
||||||
class BurglarAlarmLogic extends BaseGetXController{
|
class BurglarAlarmLogic extends BaseGetXController {
|
||||||
BurglarAlarmState state = BurglarAlarmState();
|
BurglarAlarmState state = BurglarAlarmState();
|
||||||
|
|
||||||
// 配置锁的常开模式设置 -> 防撬报警
|
// 配置锁的常开模式设置 -> 防撬报警
|
||||||
Future<void> _setLockSetGeneralSetting() async{
|
Future<void> _setLockSetGeneralSetting() async {
|
||||||
final LoginEntity entity = await ApiRepository.to.setBurglarAlarmData(
|
final LoginEntity entity = await ApiRepository.to.setBurglarAlarmData(
|
||||||
lockId: state.lockSetInfoData.value.lockId!,
|
lockId: state.lockSetInfoData.value.lockId!,
|
||||||
antiPrySwitch:state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭;
|
antiPrySwitch: state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭;
|
||||||
);
|
);
|
||||||
if(entity.errorCode!.codeIsSuccessful){
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
// eventBus.fire(RefreshLockListInfoDataEvent());
|
eventBus.fire(RefreshLockListInfoDataEvent());
|
||||||
|
|
||||||
state.burglarAlarmEnable.value = state.burglarAlarmEnable.value == 1 ? 0 : 1;
|
state.burglarAlarmEnable.value =
|
||||||
state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = state.burglarAlarmEnable.value;
|
state.burglarAlarmEnable.value == 1 ? 0 : 1;
|
||||||
showToast('操作成功'.tr, something: (){
|
state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch =
|
||||||
eventBus.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
state.burglarAlarmEnable.value;
|
||||||
|
showToast('操作成功'.tr, something: () {
|
||||||
|
eventBus
|
||||||
|
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取解析后的数据
|
// 获取解析后的数据
|
||||||
late StreamSubscription<Reply> _replySubscription;
|
late StreamSubscription<Reply> _replySubscription;
|
||||||
|
|
||||||
void _initReplySubscription() {
|
void _initReplySubscription() {
|
||||||
_replySubscription = EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
_replySubscription =
|
||||||
if(reply is SetSupportFunctionsNoParametersReply) {
|
EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
||||||
|
if (reply is SetSupportFunctionsNoParametersReply) {
|
||||||
_replySetSupportFunctionsWithParameters(reply);
|
_replySetSupportFunctionsWithParameters(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +75,7 @@ class BurglarAlarmLogic extends BaseGetXController{
|
|||||||
// 设置自动落锁数据解析
|
// 设置自动落锁数据解析
|
||||||
Future<void> _replySetSupportFunctionsWithParameters(Reply reply) async {
|
Future<void> _replySetSupportFunctionsWithParameters(Reply reply) async {
|
||||||
final int status = reply.data[2];
|
final int status = reply.data[2];
|
||||||
switch(status){
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
//成功
|
//成功
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
@ -91,41 +95,47 @@ class BurglarAlarmLogic extends BaseGetXController{
|
|||||||
|
|
||||||
// 设置支持功能(带参数)
|
// 设置支持功能(带参数)
|
||||||
Future<void> sendBurglarAlarm() async {
|
Future<void> sendBurglarAlarm() async {
|
||||||
if(state.sureBtnState.value == 1){
|
if (state.sureBtnState.value == 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.sureBtnState.value = 1;
|
state.sureBtnState.value = 1;
|
||||||
|
|
||||||
EasyLoading.show();
|
EasyLoading.show();
|
||||||
showBlueConnetctToastTimer(action: (){
|
showBlueConnetctToastTimer(action: () {
|
||||||
dismissEasyLoading();
|
dismissEasyLoading();
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
});
|
});
|
||||||
BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async {
|
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||||
|
(BluetoothConnectionState connectionState) async {
|
||||||
if (connectionState == BluetoothConnectionState.connected) {
|
if (connectionState == BluetoothConnectionState.connected) {
|
||||||
final List<String>? privateKey = await Storage.getStringList(saveBluePrivateKey);
|
final List<String>? privateKey =
|
||||||
final List<int> getPrivateKeyList = changeStringListToIntList(privateKey!);
|
await Storage.getStringList(saveBluePrivateKey);
|
||||||
|
final List<int> getPrivateKeyList =
|
||||||
|
changeStringListToIntList(privateKey!);
|
||||||
|
|
||||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||||
|
|
||||||
final List<String>? publicKey = await Storage.getStringList(saveBluePublicKey);
|
final List<String>? publicKey =
|
||||||
final List<int> getPublicKeyList = changeStringListToIntList(publicKey!);
|
await Storage.getStringList(saveBluePublicKey);
|
||||||
|
final List<int> getPublicKeyList =
|
||||||
|
changeStringListToIntList(publicKey!);
|
||||||
|
|
||||||
IoSenderManage.setSupportFunctionsNoParametersCommand(
|
IoSenderManage.setSupportFunctionsNoParametersCommand(
|
||||||
keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(),
|
keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(),
|
||||||
userID: await Storage.getUid(),
|
userID: await Storage.getUid(),
|
||||||
featureBit: 30,
|
featureBit: 30,
|
||||||
featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1,
|
featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1,
|
||||||
token: getTokenList,
|
token: getTokenList,
|
||||||
needAuthor: 1,
|
needAuthor: 1,
|
||||||
publicKey: getPublicKeyList,
|
publicKey: getPublicKeyList,
|
||||||
privateKey: getPrivateKeyList);
|
privateKey: getPrivateKeyList,
|
||||||
|
);
|
||||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||||
dismissEasyLoading();
|
dismissEasyLoading();
|
||||||
cancelBlueConnetctToastTimer();
|
cancelBlueConnetctToastTimer();
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
if(state.ifCurrentScreen.value == true){
|
if (state.ifCurrentScreen.value == true) {
|
||||||
showBlueConnetctToast();
|
showBlueConnetctToast();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,5 +162,4 @@ class BurglarAlarmLogic extends BaseGetXController{
|
|||||||
|
|
||||||
_replySubscription.cancel();
|
_replySubscription.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,40 +37,46 @@ import 'configuringWifi_state.dart';
|
|||||||
|
|
||||||
class ConfiguringWifiLogic extends BaseGetXController {
|
class ConfiguringWifiLogic extends BaseGetXController {
|
||||||
final ConfiguringWifiState state = ConfiguringWifiState();
|
final ConfiguringWifiState state = ConfiguringWifiState();
|
||||||
|
final int _configurationTimeout = 60; // 配网超时时间(秒)
|
||||||
|
|
||||||
|
/// 获取WiFi锁服务IP和端口
|
||||||
Future<void> getWifiLockServiceIpAndPort() async {
|
Future<void> getWifiLockServiceIpAndPort() async {
|
||||||
final ConfiguringWifiEntity entity =
|
try {
|
||||||
await ApiRepository.to.getWifiLockServiceIpAndPort();
|
final ConfiguringWifiEntity entity =
|
||||||
if (entity.errorCode! == 0) {
|
await ApiRepository.to.getWifiLockServiceIpAndPort();
|
||||||
state.configuringWifiEntity.value = entity;
|
if (entity.errorCode! == 0) {
|
||||||
|
state.configuringWifiEntity.value = entity;
|
||||||
|
} else {
|
||||||
|
AppLog.log('获取WiFi锁服务IP和端口失败:${entity.errorCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('获取WiFi锁服务IP和端口异常:$e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateNetworkInfo({
|
/// 更新网络信息到服务器
|
||||||
|
Future<LoginEntity> updateNetworkInfo({
|
||||||
required String peerId,
|
required String peerId,
|
||||||
required String wifiName,
|
required String wifiName,
|
||||||
required String secretKey,
|
required String secretKey,
|
||||||
required String deviceMac,
|
required String deviceMac,
|
||||||
required String networkMac,
|
required String networkMac,
|
||||||
}) async {
|
}) async {
|
||||||
final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork(
|
try {
|
||||||
deviceType: 2,
|
final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork(
|
||||||
deviceMac: deviceMac,
|
deviceType: 2,
|
||||||
wifiName: wifiName,
|
deviceMac: deviceMac,
|
||||||
networkMac: networkMac,
|
|
||||||
secretKey: secretKey,
|
|
||||||
peerId: peerId,
|
|
||||||
);
|
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
|
||||||
// 设置锁的peerID
|
|
||||||
StartChartManage().lockNetworkInfo = DeviceNetworkInfo(
|
|
||||||
wifiName: wifiName,
|
wifiName: wifiName,
|
||||||
networkMac: networkMac,
|
networkMac: networkMac,
|
||||||
secretKey: secretKey,
|
secretKey: secretKey,
|
||||||
peerId: peerId,
|
peerId: peerId,
|
||||||
);
|
);
|
||||||
|
return entity;
|
||||||
await _getUploadLockSet();
|
} catch (e) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
|
AppLog.log('网络配置异常:$e');
|
||||||
|
return LoginEntity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,81 +90,165 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
if (reply is GatewayConfiguringWifiResultReply) {
|
if (reply is GatewayConfiguringWifiResultReply) {
|
||||||
_replySenderConfiguringWifiResult(reply);
|
_replySenderConfiguringWifiResult(reply);
|
||||||
}
|
}
|
||||||
|
// wifi配网命令应答结果
|
||||||
if (reply is GatewayConfiguringWifiReply) {
|
if (reply is GatewayConfiguringWifiReply) {
|
||||||
_replySenderConfiguringWifiResult(reply);
|
_replySenderConfiguringWifi(reply);
|
||||||
}
|
}
|
||||||
if (reply is GatewayGetStatusReply) {
|
if (reply is GatewayGetStatusReply) {
|
||||||
_replyGatewayGetStatusReply(reply);
|
_replyGatewayGetStatusReply(reply);
|
||||||
}
|
}
|
||||||
// if (reply is GatewayGetStatusReply) {
|
|
||||||
// _replyStatusInfo(reply);
|
|
||||||
// }
|
|
||||||
// 上传数据获取锁设置
|
// 上传数据获取锁设置
|
||||||
if (reply is UpdataLockSetReply) {
|
if (reply is UpdataLockSetReply) {
|
||||||
_replyUpdataLockSetReply(reply);
|
_replyUpdataLockSetReply(reply);
|
||||||
}
|
}
|
||||||
AppLog.log('蓝牙回调处理完毕${EasyLoading.isShow}');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// WIFI配网结果
|
// WIFI配网操作结果处理
|
||||||
Future<void> _replySenderConfiguringWifiResult(Reply reply) async {
|
Future<void> _replySenderConfiguringWifi(Reply reply) async {
|
||||||
final int status = reply.data[2];
|
final int status = reply.data[2];
|
||||||
// state.sureBtnState.value = 0;
|
|
||||||
|
|
||||||
// 取消loading超时定时器
|
|
||||||
state.loadingTimer?.cancel();
|
|
||||||
state.loadingTimer = null;
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
await Storage.removeLockNetWorkInfoCache();
|
AppLog.log('wifi配网命令回复结果:成功');
|
||||||
final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3];
|
|
||||||
|
|
||||||
final List<int> secretKeyList =
|
|
||||||
reply.data.sublist(5, 5 + secretKeyJsonLength);
|
|
||||||
String result = utf8String(secretKeyList);
|
|
||||||
// 解析 JSON 字符串为 Map
|
|
||||||
Map<String, dynamic> jsonMap = json.decode(result);
|
|
||||||
|
|
||||||
// 提取 peerId
|
|
||||||
String? peerId = jsonMap['peerId'];
|
|
||||||
String? wifiName = jsonMap['wifiName'];
|
|
||||||
String? secretKey = jsonMap['secretKey'];
|
|
||||||
String? deviceMac = jsonMap['deviceMac'];
|
|
||||||
String? networkMac = jsonMap['networkMac'];
|
|
||||||
|
|
||||||
/// 配网成功后,赋值锁的peerId
|
|
||||||
StartChartManage().lockPeerId = peerId ?? '';
|
|
||||||
|
|
||||||
state.isLoading.value = false;
|
|
||||||
// 保存到缓存
|
|
||||||
await Storage.saveLockNetWorkInfo(jsonMap);
|
|
||||||
// 上报服务器
|
|
||||||
updateNetworkInfo(
|
|
||||||
peerId: peerId ?? '',
|
|
||||||
wifiName: wifiName ?? '',
|
|
||||||
secretKey: secretKey ?? '',
|
|
||||||
deviceMac: deviceMac ?? '',
|
|
||||||
networkMac: networkMac ?? '');
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
//失败
|
//失败
|
||||||
dismissEasyLoading(); // 关闭loading
|
dismissEasyLoading(); // 关闭loading
|
||||||
cancelBlueConnetctToastTimer();
|
|
||||||
if (state.loadingTimer != null) {
|
|
||||||
state.loadingTimer!.cancel();
|
|
||||||
state.loadingTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast('配网失败'.tr);
|
showToast('配网失败'.tr);
|
||||||
state.isLoading.value = false;
|
state.isLoading.value = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:美化 JSON 输出
|
// WIFI配网结果处理
|
||||||
|
Future<void> _replySenderConfiguringWifiResult(Reply reply) async {
|
||||||
|
final int status = reply.data[2];
|
||||||
|
|
||||||
|
// 收到响应后,取消蓝牙超时计时器
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 0x00:
|
||||||
|
// 配网成功 - 不关闭loading,保持状态直到全部完成
|
||||||
|
await Storage.removeLockNetWorkInfoCache();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3];
|
||||||
|
final List<int> secretKeyList =
|
||||||
|
reply.data.sublist(5, 5 + secretKeyJsonLength);
|
||||||
|
String result = utf8String(secretKeyList);
|
||||||
|
|
||||||
|
AppLog.log('解析配网信息: $result');
|
||||||
|
|
||||||
|
// 解析 JSON 字符串为 Map
|
||||||
|
Map<String, dynamic> jsonMap = json.decode(result);
|
||||||
|
|
||||||
|
// 提取网络信息
|
||||||
|
String? peerId = jsonMap['peerId'];
|
||||||
|
String? wifiName = jsonMap['wifiName'];
|
||||||
|
String? secretKey = jsonMap['secretKey'];
|
||||||
|
String? deviceMac = jsonMap['deviceMac'];
|
||||||
|
String? networkMac = jsonMap['networkMac'];
|
||||||
|
|
||||||
|
// 验证关键字段
|
||||||
|
if (peerId == null ||
|
||||||
|
peerId.isEmpty ||
|
||||||
|
secretKey == null ||
|
||||||
|
secretKey.isEmpty) {
|
||||||
|
throw Exception('Missing required network information');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置
|
||||||
|
final info = await updateNetworkInfo(
|
||||||
|
peerId: peerId,
|
||||||
|
wifiName: wifiName ?? '',
|
||||||
|
secretKey: secretKey,
|
||||||
|
deviceMac: deviceMac ?? '',
|
||||||
|
networkMac: networkMac ?? '');
|
||||||
|
if (info.errorCode!.codeIsSuccessful) {
|
||||||
|
// 设置锁的peerID
|
||||||
|
StartChartManage().lockNetworkInfo = DeviceNetworkInfo(
|
||||||
|
wifiName: wifiName,
|
||||||
|
networkMac: networkMac,
|
||||||
|
secretKey: secretKey,
|
||||||
|
peerId: peerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// 配网成功后,赋值锁的peerId
|
||||||
|
StartChartManage().lockPeerId = peerId;
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
await Storage.saveLockNetWorkInfo(jsonMap);
|
||||||
|
|
||||||
|
showToast('配网成功'.tr, something: () {
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
if (state.pageName.value == 'lockSet') {
|
||||||
|
Get.close(2);
|
||||||
|
} else {
|
||||||
|
Get.offAllNamed(Routers.starLockMain);
|
||||||
|
}
|
||||||
|
eventBus.fire(SuccessfulDistributionNetwork());
|
||||||
|
eventBus.fire(RefreshLockListInfoDataEvent(clearScanDevices: true,isUnShowLoading: true));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取锁设置
|
||||||
|
_getUploadLockSet();
|
||||||
|
} else {
|
||||||
|
dismissEasyLoading();
|
||||||
|
// showToast('网络配置失败,请重试'.tr);
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
// showToast('解析配网信息失败,请重试'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
AppLog.log('解析配网信息失败: $e');
|
||||||
|
return; // 添加return阻止后续流程
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x01:
|
||||||
|
// WiFi密码错误
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
// showToast('WiFi密码错误,请重新输入'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x02:
|
||||||
|
// 找不到WiFi
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
// showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x03:
|
||||||
|
// 网络连接超时
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
// showToast('连接WiFi超时,请确保网络信号良好'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 其他错误
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
// showToast('配网失败 (错误码: $status),请重试'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:美化 JSON 输出
|
||||||
String prettyPrintJson(String jsonString) {
|
String prettyPrintJson(String jsonString) {
|
||||||
var jsonObject = json.decode(jsonString);
|
var jsonObject = json.decode(jsonString);
|
||||||
return JsonEncoder.withIndent(' ').convert(jsonObject);
|
return JsonEncoder.withIndent(' ').convert(jsonObject);
|
||||||
@ -168,80 +258,111 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
Future<void> senderConfiguringWifiAction() async {
|
Future<void> senderConfiguringWifiAction() async {
|
||||||
AppLog.log('开始配网${EasyLoading.isShow}');
|
AppLog.log('开始配网${EasyLoading.isShow}');
|
||||||
|
|
||||||
if (state.isLoading.isTrue) {
|
if (state.sureBtnState.value == 1) {
|
||||||
AppLog.log('正在配网中请勿重复点击');
|
AppLog.log('正在配网中请勿重复点击');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.wifiNameController.text.isEmpty) {
|
|
||||||
showToast('请输入wifi名称'.tr);
|
// 获取网关配置信息
|
||||||
|
try {
|
||||||
|
final GetGatewayConfigurationEntity entity = await ApiRepository.to
|
||||||
|
.getGatewayConfigurationNotLoading(timeout: _configurationTimeout);
|
||||||
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
|
state.getGatewayConfigurationStr = entity.data ?? '';
|
||||||
|
} else {
|
||||||
|
// showToast('获取网关配置失败,请重试'.tr);
|
||||||
|
AppLog.log('获取网关配置失败,请重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否登录账户
|
||||||
|
final loginData = await Storage.getLoginData();
|
||||||
|
if (loginData == null) {
|
||||||
|
AppLog.log('未检测到登录信息,请重新登录'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取app用户的peerId
|
||||||
|
String appPeerId = loginData.starchart?.starchartId ?? '';
|
||||||
|
if (appPeerId.isEmpty) {
|
||||||
|
AppLog.log('用户ID获取失败,请重新登录'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理配置字符串
|
||||||
|
if (state.getGatewayConfigurationStr.isNotEmpty) {
|
||||||
|
// 解析 JSON 字符串为 Map
|
||||||
|
Map<String, dynamic> jsonMap =
|
||||||
|
json.decode(state.getGatewayConfigurationStr);
|
||||||
|
|
||||||
|
// 移除指定的键
|
||||||
|
jsonMap.remove("starCloudUrl");
|
||||||
|
jsonMap.remove("starLockPeerId");
|
||||||
|
|
||||||
|
// 追加新的键值对
|
||||||
|
jsonMap['userPeerld'] = appPeerId;
|
||||||
|
|
||||||
|
// 将 Map 转换回 JSON 字符串
|
||||||
|
state.getGatewayConfigurationStr =
|
||||||
|
json.encode(jsonMap).replaceAll(',', ',\n');
|
||||||
|
|
||||||
|
// 确保格式化输出
|
||||||
|
state.getGatewayConfigurationStr =
|
||||||
|
prettyPrintJson(state.getGatewayConfigurationStr);
|
||||||
|
} else {
|
||||||
|
// 如果为空,则直接赋值
|
||||||
|
state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('网关配置准备失败:${e.toString()}'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.wifiPWDController.text.isEmpty) {
|
// 先设置sureBtnState状态,以禁用按钮
|
||||||
showToast('请输入WiFi密码'.tr);
|
state.sureBtnState.value = 1;
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if (state.sureBtnState.value == 1) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// state.sureBtnState.value = 1;
|
|
||||||
|
|
||||||
final GetGatewayConfigurationEntity entity =
|
// 显示loading,如果已经显示则不再重复显示
|
||||||
await ApiRepository.to.getGatewayConfigurationNotLoading(timeout: 60);
|
if (!EasyLoading.isShow) {
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
showEasyLoading();
|
||||||
state.getGatewayConfigurationStr = entity.data ?? '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否登录账户
|
// 设置蓝牙操作超时处理
|
||||||
final loginData = await Storage.getLoginData();
|
showBlueConnetctToastTimer(
|
||||||
|
action: () {
|
||||||
// 获取app用户的peerId
|
if (EasyLoading.isShow) {
|
||||||
String appPeerId = loginData?.starchart?.starchartId ?? '';
|
dismissEasyLoading();
|
||||||
// 如果已有值,则追加
|
}
|
||||||
if (state.getGatewayConfigurationStr.isNotEmpty) {
|
state.sureBtnState.value = 0; // 连接超时时重置状态
|
||||||
// 解析 JSON 字符串为 Map
|
},
|
||||||
Map<String, dynamic> jsonMap =
|
outTimer: 30,
|
||||||
json.decode(state.getGatewayConfigurationStr);
|
);
|
||||||
|
|
||||||
// 移除指定的键
|
|
||||||
jsonMap.remove("starCloudUrl");
|
|
||||||
jsonMap.remove("starLockPeerId");
|
|
||||||
|
|
||||||
// 追加新的键值对
|
|
||||||
jsonMap['userPeerld'] = appPeerId;
|
|
||||||
|
|
||||||
// 将 Map 转换回 JSON 字符串
|
|
||||||
state.getGatewayConfigurationStr =
|
|
||||||
json.encode(jsonMap).replaceAll(',', ',\n');
|
|
||||||
|
|
||||||
// 确保格式化输出
|
|
||||||
state.getGatewayConfigurationStr =
|
|
||||||
prettyPrintJson(state.getGatewayConfigurationStr);
|
|
||||||
} else {
|
|
||||||
// 如果为空,则直接赋值
|
|
||||||
state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}";
|
|
||||||
}
|
|
||||||
showEasyLoading();
|
|
||||||
showBlueConnetctToastTimer(action: () {
|
|
||||||
dismissEasyLoading();
|
|
||||||
state.isLoading.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送配网指令
|
// 发送配网指令
|
||||||
BlueManage().blueSendData(
|
BlueManage().blueSendData(
|
||||||
BlueManage().connectDeviceName,
|
BlueManage().connectDeviceName,
|
||||||
(BluetoothConnectionState connectionState) async {
|
(BluetoothConnectionState connectionState) async {
|
||||||
if (connectionState == BluetoothConnectionState.connected) {
|
if (connectionState == BluetoothConnectionState.connected) {
|
||||||
IoSenderManage.gatewayConfiguringWifiCommand(
|
try {
|
||||||
ssid: state.wifiNameController.text,
|
IoSenderManage.gatewayConfiguringWifiCommand(
|
||||||
password: state.wifiPWDController.text,
|
ssid: state.wifiNameController.text,
|
||||||
gatewayConfigurationStr: state.getGatewayConfigurationStr,
|
password: state.wifiPWDController.text,
|
||||||
);
|
gatewayConfigurationStr: state.getGatewayConfigurationStr,
|
||||||
|
);
|
||||||
|
// 注意:此处不要重置sureBtnState状态,等待配网结果回调
|
||||||
|
} catch (e) {
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
state.sureBtnState.value = 0; // 发送命令失败时重置状态
|
||||||
|
// showToast('发送配网指令失败:${e.toString()}'.tr);
|
||||||
|
}
|
||||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||||
dismissEasyLoading();
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
}
|
||||||
cancelBlueConnetctToastTimer();
|
cancelBlueConnetctToastTimer();
|
||||||
state.isLoading.value = false;
|
state.sureBtnState.value = 0; // 蓝牙断开时重置状态
|
||||||
// state.sureBtnState.value = 0;
|
|
||||||
if (state.ifCurrentScreen.value == true) {
|
if (state.ifCurrentScreen.value == true) {
|
||||||
showBlueConnetctToast();
|
showBlueConnetctToast();
|
||||||
}
|
}
|
||||||
@ -249,7 +370,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
},
|
},
|
||||||
isAddEquipment: false,
|
isAddEquipment: false,
|
||||||
);
|
);
|
||||||
state.isLoading.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取设备状态
|
// 获取设备状态
|
||||||
@ -278,11 +398,15 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
final NetworkInfo _networkInfo = NetworkInfo();
|
final NetworkInfo _networkInfo = NetworkInfo();
|
||||||
|
|
||||||
Future<String> getWifiName() async {
|
Future<String> getWifiName() async {
|
||||||
String ssid = '';
|
try {
|
||||||
ssid = (await _networkInfo.getWifiName())!;
|
String? ssid = await _networkInfo.getWifiName();
|
||||||
ssid = ssid ?? '';
|
ssid = ssid ?? '';
|
||||||
ssid = ssid.replaceAll(r'"', '');
|
ssid = ssid.replaceAll(r'"', '');
|
||||||
return ssid ?? '';
|
return ssid;
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('获取WiFi名称失败: $e');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///定位权限
|
///定位权限
|
||||||
@ -308,17 +432,12 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
|
|
||||||
getWifiLockServiceIpAndPort();
|
getWifiLockServiceIpAndPort();
|
||||||
_initReplySubscription();
|
_initReplySubscription();
|
||||||
// getDevicesStatusAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_replySubscription.cancel();
|
_replySubscription.cancel();
|
||||||
|
cancelBlueConnetctToastTimer(); // 确保取消蓝牙超时计时器
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,8 +449,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
//成功
|
//成功
|
||||||
// state.sureBtnState.value = 0;
|
|
||||||
|
|
||||||
final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel();
|
final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel();
|
||||||
// 网关MAC地址
|
// 网关MAC地址
|
||||||
int index = 3;
|
int index = 3;
|
||||||
@ -372,25 +489,16 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
default:
|
default:
|
||||||
//失败
|
//失败
|
||||||
dismissEasyLoading();
|
dismissEasyLoading();
|
||||||
showToast('配网失败'.tr);
|
// showToast('获取设备状态失败'.tr);
|
||||||
if (state.loadingTimer != null) {
|
|
||||||
state.loadingTimer!.cancel();
|
|
||||||
state.loadingTimer = null;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传数据获取设置
|
// 上传数据获取设置
|
||||||
Future<void> _getUploadLockSet() async {
|
Future<void> _getUploadLockSet() async {
|
||||||
showEasyLoading();
|
|
||||||
showBlueConnetctToastTimer(action: () {
|
|
||||||
dismissEasyLoading();
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||||
|
// 蓝牙获取锁设置
|
||||||
await _uploadLockSet(getTokenList);
|
await _uploadLockSet(getTokenList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,44 +506,74 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
Future<void> _uploadLockSet(List<int> token) async {
|
Future<void> _uploadLockSet(List<int> token) async {
|
||||||
final List<String>? privateKey =
|
final List<String>? privateKey =
|
||||||
await Storage.getStringList(saveBluePrivateKey);
|
await Storage.getStringList(saveBluePrivateKey);
|
||||||
final List<int> getPrivateKeyList = changeStringListToIntList(privateKey!);
|
if (privateKey == null || privateKey.isEmpty) {
|
||||||
|
throw Exception('Private key is empty');
|
||||||
|
}
|
||||||
|
final List<int> getPrivateKeyList = changeStringListToIntList(privateKey);
|
||||||
|
|
||||||
final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
||||||
final List<int> signKeyDataList = changeStringListToIntList(signKey!);
|
if (signKey == null || signKey.isEmpty) {
|
||||||
|
throw Exception('Sign key is empty');
|
||||||
|
}
|
||||||
|
final List<int> signKeyDataList = changeStringListToIntList(signKey);
|
||||||
|
|
||||||
IoSenderManage.updataLockSetCommand(
|
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||||
lockID: BlueManage().connectDeviceName,
|
(BluetoothConnectionState connectionState) async {
|
||||||
userID: await Storage.getUid(),
|
if (connectionState == BluetoothConnectionState.connected) {
|
||||||
token: token,
|
IoSenderManage.updataLockSetCommand(
|
||||||
needAuthor: 1,
|
lockID: BlueManage().connectDeviceName,
|
||||||
signKey: signKeyDataList,
|
userID: await Storage.getUid(),
|
||||||
privateKey: getPrivateKeyList);
|
token: token,
|
||||||
|
needAuthor: 1,
|
||||||
|
signKey: signKeyDataList,
|
||||||
|
privateKey: getPrivateKeyList,
|
||||||
|
);
|
||||||
|
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||||
|
dismissEasyLoading();
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
if (state.ifCurrentScreen.value == true) {
|
||||||
|
showBlueConnetctToast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, isAddEquipment: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传数据获取锁设置解析
|
// 上传数据获取锁设置解析
|
||||||
Future<void> _replyUpdataLockSetReply(Reply reply) async {
|
Future<void> _replyUpdataLockSetReply(Reply reply) async {
|
||||||
final int status = reply.data[2];
|
final int status = reply.data[2];
|
||||||
dismissEasyLoading(); // 关闭loading
|
// 保持loading状态直到整个过程完成
|
||||||
cancelBlueConnetctToastTimer();
|
cancelBlueConnetctToastTimer();
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
await _lockDataUpload(
|
await _lockDataUpload(
|
||||||
uploadType: 1,
|
uploadType: 1,
|
||||||
recordType: 0,
|
recordType: 0,
|
||||||
records: reply.data.sublist(7, reply.data.length));
|
records: reply.data.sublist(7, reply.data.length));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x06:
|
case 0x06:
|
||||||
//无权限
|
//无权限,尝试重新获取token
|
||||||
final List<int> token = reply.data.sublist(3, 7);
|
try {
|
||||||
final List<String> saveStrList = changeIntListToStringList(token);
|
final List<int> token = reply.data.sublist(3, 7);
|
||||||
Storage.setStringList(saveBlueToken, saveStrList);
|
final List<String> saveStrList = changeIntListToStringList(token);
|
||||||
|
await Storage.setStringList(saveBlueToken, saveStrList);
|
||||||
_uploadLockSet(token);
|
_uploadLockSet(token);
|
||||||
|
} catch (e) {
|
||||||
|
if (EasyLoading.isShow) {
|
||||||
|
dismissEasyLoading(); // 错误时关闭loading
|
||||||
|
}
|
||||||
|
// showToast('获取设置权限失败:${e.toString()}'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
dismissEasyLoading();
|
if (EasyLoading.isShow) {
|
||||||
cancelBlueConnetctToastTimer();
|
dismissEasyLoading(); // 错误时关闭loading
|
||||||
|
}
|
||||||
|
// showToast('获取锁设置失败 (错误码: $status)'.tr);
|
||||||
|
state.sureBtnState.value = 0; // 确保重置状态
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -451,19 +589,10 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
recordType: recordType,
|
recordType: recordType,
|
||||||
records: records,
|
records: records,
|
||||||
isUnShowLoading: true);
|
isUnShowLoading: true);
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
|
||||||
showToast('配网成功'.tr, something: () {
|
|
||||||
state.isLoading.value = false;
|
|
||||||
if (state.pageName.value == 'lockSet') {
|
|
||||||
Get.close(2);
|
|
||||||
} else {
|
|
||||||
Get.offAllNamed(Routers.starLockMain);
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
eventBus
|
||||||
eventBus.fire(SuccessfulDistributionNetwork());
|
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,9 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
|||||||
final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic());
|
final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic());
|
||||||
final ConfiguringWifiState state = Get.find<ConfiguringWifiLogic>().state;
|
final ConfiguringWifiState state = Get.find<ConfiguringWifiLogic>().state;
|
||||||
|
|
||||||
|
// 添加密码可见性控制
|
||||||
|
final RxBool _obscureText = true.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -39,19 +42,36 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
|||||||
'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController),
|
'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController),
|
||||||
Container(
|
Container(
|
||||||
width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor),
|
width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor),
|
||||||
configuringWifiTFWidget(
|
configuringWifiPasswordTFWidget(
|
||||||
'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController),
|
'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 50.h,
|
height: 50.h,
|
||||||
),
|
),
|
||||||
Obx(
|
Obx(
|
||||||
() => SubmitBtn(
|
() => SubmitBtn(
|
||||||
btnName: '确定'.tr,
|
btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr,
|
||||||
isDisabled: state.isLoading.isFalse,
|
// 当sureBtnState为1时按钮不可用
|
||||||
onClick: state.isLoading.isTrue
|
isDisabled: state.sureBtnState.value == 0,
|
||||||
|
onClick: state.sureBtnState.value == 1
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
FocusScope.of(context).requestFocus(FocusNode());
|
FocusScope.of(context).requestFocus(FocusNode());
|
||||||
|
// 验证输入
|
||||||
|
if (state.wifiNameController.text.isEmpty) {
|
||||||
|
logic.showToast('请输入WiFi名称'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.wifiPWDController.text.isEmpty) {
|
||||||
|
logic.showToast('请输入WiFi密码'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检查WiFi名称是否包含5G关键字
|
||||||
|
if (state.wifiNameController.text
|
||||||
|
.toLowerCase()
|
||||||
|
.contains('5g')) {
|
||||||
|
logic.showToast('请确保使用2.4GHz WiFi网络'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
logic.senderConfiguringWifiAction();
|
logic.senderConfiguringWifiAction();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -86,7 +106,22 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接受者信息输入框
|
Widget configuringWifiPasswordTFWidget(
|
||||||
|
String titleStr, String rightTitle, TextEditingController controller) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 10.h),
|
||||||
|
CommonItem(
|
||||||
|
leftTitel: titleStr,
|
||||||
|
rightTitle: '',
|
||||||
|
isHaveRightWidget: true,
|
||||||
|
rightWidget: getPasswordTFWidget(rightTitle, controller)),
|
||||||
|
Container(height: 10.h),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通输入框
|
||||||
Widget getTFWidget(String tfStr, TextEditingController controller) {
|
Widget getTFWidget(String tfStr, TextEditingController controller) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 65.h,
|
height: 65.h,
|
||||||
@ -95,18 +130,14 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
//输入框一行
|
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
FilteringTextInputFormatter.deny('\n'),
|
FilteringTextInputFormatter.deny('\n'),
|
||||||
// LengthLimitingTextInputFormatter(30),
|
|
||||||
],
|
],
|
||||||
controller: controller,
|
controller: controller,
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
//输入里面输入文字内边距设置
|
|
||||||
// contentPadding: const EdgeInsets.only(top: 12.0, bottom: 8.0),
|
|
||||||
hintText: tfStr,
|
hintText: tfStr,
|
||||||
hintStyle: TextStyle(fontSize: 22.sp),
|
hintStyle: TextStyle(fontSize: 22.sp),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
@ -135,6 +166,61 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 密码输入框
|
||||||
|
Widget getPasswordTFWidget(String tfStr, TextEditingController controller) {
|
||||||
|
return Container(
|
||||||
|
height: 65.h,
|
||||||
|
width: 300.w,
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => TextField(
|
||||||
|
maxLines: 1,
|
||||||
|
obscureText: _obscureText.value,
|
||||||
|
inputFormatters: <TextInputFormatter>[
|
||||||
|
FilteringTextInputFormatter.deny('\n'),
|
||||||
|
],
|
||||||
|
controller: controller,
|
||||||
|
autofocus: false,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: tfStr,
|
||||||
|
hintStyle: TextStyle(fontSize: 22.sp),
|
||||||
|
focusedBorder: const OutlineInputBorder(
|
||||||
|
borderSide:
|
||||||
|
BorderSide(width: 0, color: Colors.transparent)),
|
||||||
|
disabledBorder: const OutlineInputBorder(
|
||||||
|
borderSide:
|
||||||
|
BorderSide(width: 0, color: Colors.transparent)),
|
||||||
|
enabledBorder: const OutlineInputBorder(
|
||||||
|
borderSide:
|
||||||
|
BorderSide(width: 0, color: Colors.transparent)),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderSide:
|
||||||
|
BorderSide(width: 0, color: Colors.transparent)),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22.sp, textBaseline: TextBaseline.alphabetic),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_obscureText.value ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 24.sp,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_obscureText.value = !_obscureText.value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:network_info_plus/network_info_plus.dart';
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
@ -33,5 +31,4 @@ class ConfiguringWifiState {
|
|||||||
String getGatewayConfigurationStr = '';
|
String getGatewayConfigurationStr = '';
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
Timer? loadingTimer;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
import 'package:star_lock/blue/io_gateway/io_gateway_getWifiList.dart';
|
import 'package:star_lock/blue/io_gateway/io_gateway_getWifiList.dart';
|
||||||
import 'package:star_lock/blue/io_protocol/io_getWifiList.dart';
|
import 'package:star_lock/blue/io_protocol/io_getWifiList.dart';
|
||||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
@ -20,7 +21,9 @@ class WifiListLogic extends BaseGetXController {
|
|||||||
|
|
||||||
// 获取解析后的数据
|
// 获取解析后的数据
|
||||||
late StreamSubscription<Reply> _replySubscription;
|
late StreamSubscription<Reply> _replySubscription;
|
||||||
|
Timer? _connectionTimer;
|
||||||
|
|
||||||
|
/// 初始化订阅,监听设备响应
|
||||||
void _initReplySubscription() {
|
void _initReplySubscription() {
|
||||||
_replySubscription =
|
_replySubscription =
|
||||||
EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
||||||
@ -31,78 +34,130 @@ class WifiListLogic extends BaseGetXController {
|
|||||||
if (reply is GatewayGetWifiListReply) {
|
if (reply is GatewayGetWifiListReply) {
|
||||||
_replyGetWifiListParameters(reply);
|
_replyGetWifiListParameters(reply);
|
||||||
}
|
}
|
||||||
|
}, onError: (error) {
|
||||||
|
// 处理CRC校验失败等错误
|
||||||
|
AppLog.log('WiFi列表获取过程中发生错误: $error');
|
||||||
|
|
||||||
|
// 取消loading状态,显示错误提示
|
||||||
|
dismissEasyLoading();
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
|
||||||
|
// 重置按钮状态,允许重新扫描
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
|
|
||||||
|
// 如果是CRC校验失败,显示特定提示
|
||||||
|
if (error.toString().contains('CRC')) {
|
||||||
|
showToast('数据校验失败,请重新扫描'.tr);
|
||||||
|
} else {
|
||||||
|
showToast('扫描WiFi失败,请重试'.tr);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送获取wifi列表数据解析
|
/// 发送获取wifi列表数据解析
|
||||||
Future<void> _replySendGetWifiParameters(Reply reply) async {
|
Future<void> _replySendGetWifiParameters(Reply reply) async {
|
||||||
final int status = reply.data[2];
|
final int status = reply.data[2];
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
//成功
|
//成功 - 不显示loading框,UI中已经有进度指示器
|
||||||
showEasyLoading();
|
|
||||||
cancelBlueConnetctToastTimer();
|
cancelBlueConnetctToastTimer();
|
||||||
Future.delayed(5.seconds, dismissEasyLoading);
|
|
||||||
break;
|
break;
|
||||||
case 0x06:
|
case 0x06:
|
||||||
// 需要鉴权
|
// 需要鉴权
|
||||||
|
dismissEasyLoading();
|
||||||
|
AppLog.log('需要设备鉴权,请重试'.tr);
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// 处理其他错误状态
|
||||||
|
dismissEasyLoading();
|
||||||
|
AppLog.log('获取WiFi列表失败,错误码:$status'.tr);
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取WiFi数据解析
|
/// 获取WiFi数据解析
|
||||||
Future<void> _replyGetWifiListParameters(Reply reply) async {
|
Future<void> _replyGetWifiListParameters(Reply reply) async {
|
||||||
final int status = reply.data[2];
|
final int status = reply.data[2];
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0x00:
|
case 0x00:
|
||||||
//成功
|
//成功
|
||||||
// showEasyLoading();
|
|
||||||
dismissEasyLoading();
|
dismissEasyLoading();
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
|
|
||||||
if (reply.data[3] > 0) {
|
if (reply.data[3] > 0) {
|
||||||
reply.data.removeRange(0, 4);
|
reply.data.removeRange(0, 4);
|
||||||
// 把得到的数据按33位分割成数组 然后塞进一个新的数组里面
|
// 把得到的数据按33位分割成数组然后处理
|
||||||
final List<List<int>> getList = splitList(reply.data, 33);
|
final List<List<int>> getList = splitList(reply.data, 33);
|
||||||
final List<Map<String, String>> uploadList = <Map<String, String>>[];
|
final List<Map<String, String>> uploadList = <Map<String, String>>[];
|
||||||
|
|
||||||
for (int i = 0; i < getList.length; i++) {
|
for (int i = 0; i < getList.length; i++) {
|
||||||
final List<int> indexList = getList[i];
|
final List<int> indexList = getList[i];
|
||||||
final Map<String, String> indexMap = <String, String>{};
|
final Map<String, String> indexMap = <String, String>{};
|
||||||
final List<int> wifiName = indexList.sublist(0, 32);
|
final List<int> wifiName = indexList.sublist(0, 32);
|
||||||
indexMap['wifiName'] = utf8String(wifiName);
|
final String wifiNameStr = utf8String(wifiName).trim();
|
||||||
|
|
||||||
|
// 过滤掉空的WiFi名称
|
||||||
|
if (wifiNameStr.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMap['wifiName'] = wifiNameStr;
|
||||||
indexMap['rssi'] = (indexList.last - 255).toString();
|
indexMap['rssi'] = (indexList.last - 255).toString();
|
||||||
uploadList.add(indexMap);
|
uploadList.add(indexMap);
|
||||||
state.wifiNameDataList.value = uploadList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按信号强度排序WiFi列表 (从强到弱)
|
||||||
|
uploadList.sort(
|
||||||
|
(a, b) => int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!)));
|
||||||
|
|
||||||
|
state.wifiNameDataList.value = uploadList;
|
||||||
|
|
||||||
|
if (uploadList.isEmpty) {
|
||||||
|
showToast('未检测到可用的WiFi网络'.tr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 处理WiFi列表为空的情况
|
||||||
|
state.wifiNameDataList.clear();
|
||||||
|
showToast('未检测到可用的WiFi网络'.tr);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// 处理其他错误状态
|
||||||
|
dismissEasyLoading();
|
||||||
|
showToast('解析WiFi列表失败,错误码:$status'.tr);
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取wifi列表
|
/// 获取WiFi列表
|
||||||
Future<void> senderGetWifiListWifiAction() async {
|
Future<void> senderGetWifiListWifiAction() async {
|
||||||
if (state.sureBtnState.value == 1) {
|
if (state.sureBtnState.value == 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.sureBtnState.value = 1;
|
state.sureBtnState.value = 1;
|
||||||
|
state.wifiNameDataList.clear(); // 清空之前的列表
|
||||||
|
|
||||||
showEasyLoading();
|
// 不显示loading框,UI中已经有进度指示器
|
||||||
showBlueConnetctToastTimer(action: () {
|
showBlueConnetctToastTimer(action: () {
|
||||||
dismissEasyLoading();
|
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||||
(BluetoothConnectionState connectionState) async {
|
(BluetoothConnectionState connectionState) async {
|
||||||
if (connectionState == BluetoothConnectionState.connected) {
|
if (connectionState == BluetoothConnectionState.connected) {
|
||||||
IoSenderManage.gatewayGetWifiCommand(
|
try {
|
||||||
userID: await Storage.getUid(),
|
IoSenderManage.gatewayGetWifiCommand(
|
||||||
);
|
userID: await Storage.getUid(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
state.sureBtnState.value = 0;
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
showToast('发送获取WiFi列表请求失败:${e.toString()}'.tr);
|
||||||
|
}
|
||||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||||
dismissEasyLoading();
|
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
cancelBlueConnetctToastTimer();
|
cancelBlueConnetctToastTimer();
|
||||||
if (state.ifCurrentScreen.value == true) {
|
if (state.ifCurrentScreen.value == true) {
|
||||||
@ -113,22 +168,27 @@ class WifiListLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() async {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
|
|
||||||
_initReplySubscription();
|
_initReplySubscription();
|
||||||
|
await senderGetWifiListWifiAction();
|
||||||
|
dismissEasyLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
// 页面进入时标记为当前页面
|
||||||
senderGetWifiListWifiAction();
|
state.ifCurrentScreen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
super.onClose();
|
// 取消所有计时器和订阅,防止内存泄漏
|
||||||
_replySubscription.cancel();
|
_replySubscription.cancel();
|
||||||
|
_connectionTimer?.cancel();
|
||||||
|
cancelBlueConnetctToastTimer();
|
||||||
|
state.ifCurrentScreen.value = false;
|
||||||
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,32 @@ class _WifiListPageState extends State<WifiListPage> {
|
|||||||
final WifiListLogic logic = Get.put(WifiListLogic());
|
final WifiListLogic logic = Get.put(WifiListLogic());
|
||||||
final WifiListState state = Get.find<WifiListLogic>().state;
|
final WifiListState state = Get.find<WifiListLogic>().state;
|
||||||
|
|
||||||
|
/// 计算WiFi信号强度图标
|
||||||
|
IconData _getWifiSignalIcon(String rssi) {
|
||||||
|
final int rssiValue = int.parse(rssi);
|
||||||
|
if (rssiValue >= -50) {
|
||||||
|
return Icons.signal_wifi_4_bar;
|
||||||
|
} else if (rssiValue >= -70) {
|
||||||
|
return Icons.network_wifi;
|
||||||
|
} else if (rssiValue >= -80) {
|
||||||
|
return Icons.network_wifi_2_bar_rounded;
|
||||||
|
} else {
|
||||||
|
return Icons.signal_wifi_0_bar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
state.ifCurrentScreen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
state.ifCurrentScreen.value = false;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
@ -38,13 +64,15 @@ class _WifiListPageState extends State<WifiListPage> {
|
|||||||
barTitle: 'WIFI列表'.tr,
|
barTitle: 'WIFI列表'.tr,
|
||||||
haveBack: state.pageName.value == 'lockSet',
|
haveBack: state.pageName.value == 'lockSet',
|
||||||
actionsList: <Widget>[
|
actionsList: <Widget>[
|
||||||
TextButton(
|
Obx(() => TextButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
'刷新'.tr,
|
'刷新'.tr,
|
||||||
style: TextStyle(color: Colors.white, fontSize: 24.sp),
|
style: TextStyle(color: Colors.white, fontSize: 24.sp),
|
||||||
),
|
),
|
||||||
onPressed: logic.senderGetWifiListWifiAction,
|
onPressed: state.sureBtnState.value == 0
|
||||||
),
|
? logic.senderGetWifiListWifiAction
|
||||||
|
: null,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
backgroundColor: AppColors.mainColor,
|
backgroundColor: AppColors.mainColor,
|
||||||
),
|
),
|
||||||
@ -52,26 +80,56 @@ class _WifiListPageState extends State<WifiListPage> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() => state.wifiNameDataList.value.isNotEmpty
|
child: Obx(() => state.wifiNameDataList.value.isNotEmpty
|
||||||
? ListView.builder(
|
? RefreshIndicator(
|
||||||
itemCount: state.wifiNameDataList.value.length,
|
onRefresh: () async {
|
||||||
itemBuilder: (BuildContext c, int index) {
|
if (state.sureBtnState.value == 0) {
|
||||||
Map wifiNameStr = state.wifiNameDataList.value[index];
|
await logic.senderGetWifiListWifiAction();
|
||||||
return _messageListItem(
|
}
|
||||||
wifiNameStr['wifiName'], wifiNameStr['rssi'], () {
|
},
|
||||||
Get.toNamed(Routers.configuringWifiPage,
|
child: ListView.builder(
|
||||||
arguments: {
|
itemCount: state.wifiNameDataList.value.length,
|
||||||
'lockSetInfoData':
|
itemBuilder: (BuildContext c, int index) {
|
||||||
state.lockSetInfoData.value,
|
Map wifiNameStr =
|
||||||
'wifiName': wifiNameStr['wifiName'],
|
state.wifiNameDataList.value[index];
|
||||||
'pageName': state.pageName.value,
|
return _messageListItem(
|
||||||
});
|
wifiNameStr['wifiName'], wifiNameStr['rssi'],
|
||||||
});
|
() {
|
||||||
})
|
Get.toNamed(Routers.configuringWifiPage,
|
||||||
: NoData(
|
arguments: {
|
||||||
noDataHeight: 1.sh -
|
'lockSetInfoData':
|
||||||
ScreenUtil().statusBarHeight -
|
state.lockSetInfoData.value,
|
||||||
ScreenUtil().bottomBarHeight -
|
'wifiName': wifiNameStr['wifiName'],
|
||||||
64.h)),
|
'pageName': state.pageName.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: Obx(() => state.sureBtnState.value == 1
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 4.w,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
AppColors.mainColor,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
Text('正在扫描WiFi网络...\n请确保设备处于正常状态'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24.sp,
|
||||||
|
color: AppColors.blackColor))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: NoData())),
|
||||||
),
|
),
|
||||||
state.pageName.value == 'saveLock'
|
state.pageName.value == 'saveLock'
|
||||||
? SubmitBtn(
|
? SubmitBtn(
|
||||||
@ -140,24 +198,37 @@ class _WifiListPageState extends State<WifiListPage> {
|
|||||||
height: 79.h,
|
height: 79.h,
|
||||||
width: 1.sw - 20.w * 2,
|
width: 1.sw - 20.w * 2,
|
||||||
child: Row(
|
child: Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Flexible(
|
Flexible(
|
||||||
|
flex: 4,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$wifiName(${rssi}db)',
|
wifiName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22.sp, color: AppColors.blackColor),
|
fontSize: 24.sp, color: AppColors.blackColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Text(
|
Flexible(
|
||||||
// rssi,
|
flex: 1,
|
||||||
// maxLines: 1,
|
child: Row(
|
||||||
// overflow: TextOverflow.ellipsis,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
// style: TextStyle(
|
children: [
|
||||||
// fontSize: 22.sp, color: AppColors.blackColor),
|
Icon(
|
||||||
// )
|
_getWifiSignalIcon(rssi),
|
||||||
|
color: AppColors.mainColor,
|
||||||
|
size: 24.sp,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8.w),
|
||||||
|
Text(
|
||||||
|
'$rssi dB',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 18.sp, color: Colors.black),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -118,6 +118,15 @@ class _NormallyOpenModePageState extends State<NormallyOpenModePage> with RouteA
|
|||||||
: SubmitBtn(
|
: SubmitBtn(
|
||||||
btnName: '保存'.tr,
|
btnName: '保存'.tr,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
|
if (state.weekDays.value.isEmpty) {
|
||||||
|
logic.showToast('请选择常开日期'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.endTimeMinute.value < state.beginTimeMinute.value) {
|
||||||
|
logic.showToast('结束时间不能小于开始时间哦'.tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
logic.sendAutoLock();
|
logic.sendAutoLock();
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@ -21,9 +21,10 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
|||||||
RemoteUnlockingState state = RemoteUnlockingState();
|
RemoteUnlockingState state = RemoteUnlockingState();
|
||||||
|
|
||||||
void remoteUnlockingOpenOrClose() async {
|
void remoteUnlockingOpenOrClose() async {
|
||||||
final LoginEntity entity = await ApiRepository.to.remoteUnlockingOpenOrClose(
|
final LoginEntity entity = await ApiRepository.to
|
||||||
lockId: state.lockSetInfoData.value.lockId!,
|
.remoteUnlockingOpenOrClose(
|
||||||
remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1);
|
lockId: state.lockSetInfoData.value.lockId!,
|
||||||
|
remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1);
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
showToast('操作成功'.tr, something: () {
|
showToast('操作成功'.tr, something: () {
|
||||||
eventBus.fire(RefreshLockListInfoDataEvent());
|
eventBus.fire(RefreshLockListInfoDataEvent());
|
||||||
@ -32,7 +33,6 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
|||||||
state.remoteEnable.value;
|
state.remoteEnable.value;
|
||||||
eventBus
|
eventBus
|
||||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||||
eventBus.fire(RefreshLockListInfoDataEvent());
|
|
||||||
eventBus.fire(LockSetChangeSetRefreshLockDetailWithType(
|
eventBus.fire(LockSetChangeSetRefreshLockDetailWithType(
|
||||||
5,
|
5,
|
||||||
state.lockSetInfoData.value.lockSettingInfo!.remoteUnlock!
|
state.lockSetInfoData.value.lockSettingInfo!.remoteUnlock!
|
||||||
@ -44,6 +44,7 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
|||||||
|
|
||||||
// 获取解析后的数据
|
// 获取解析后的数据
|
||||||
late StreamSubscription<Reply> _replySubscription;
|
late StreamSubscription<Reply> _replySubscription;
|
||||||
|
|
||||||
void _initReplySubscription() {
|
void _initReplySubscription() {
|
||||||
_replySubscription =
|
_replySubscription =
|
||||||
EventBusManager().eventBus!.on<Reply>().listen((reply) {
|
EventBusManager().eventBus!.on<Reply>().listen((reply) {
|
||||||
|
|||||||
@ -29,9 +29,9 @@ class CoerceOpenDoorLogic extends BaseGetXController {
|
|||||||
case 2:
|
case 2:
|
||||||
return '密码'.tr;
|
return '密码'.tr;
|
||||||
case 3:
|
case 3:
|
||||||
return '指纹'.tr;
|
return 'IC卡'.tr;
|
||||||
case 4:
|
case 4:
|
||||||
return '卡'.tr;
|
return '指纹'.tr;
|
||||||
case 5:
|
case 5:
|
||||||
return '人脸'.tr;
|
return '人脸'.tr;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -234,6 +234,7 @@ class PalmListLogic extends BaseGetXController {
|
|||||||
_initReplySubscription();
|
_initReplySubscription();
|
||||||
|
|
||||||
// _initRefreshAction();
|
// _initRefreshAction();
|
||||||
|
await getPalmListData(isRefresh: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_logic.dart';
|
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_logic.dart';
|
||||||
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_state.dart';
|
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_state.dart';
|
||||||
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart';
|
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart';
|
||||||
@ -133,7 +134,7 @@ class _PasswordKeyDetailPageState extends State<PasswordKeyDetailPage>
|
|||||||
action: () {}),
|
action: () {}),
|
||||||
Container(height: 10.h),
|
Container(height: 10.h),
|
||||||
Obx(() => Visibility(
|
Obx(() => Visibility(
|
||||||
visible: state.itemData.value.isCustom! == 1,
|
visible: state.itemData.value.isCustom! == 1 && !F.isSKY,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CommonItem(
|
CommonItem(
|
||||||
|
|||||||
@ -361,6 +361,7 @@ class Bluetooth {
|
|||||||
class LockFeature {
|
class LockFeature {
|
||||||
LockFeature({
|
LockFeature({
|
||||||
this.password,
|
this.password,
|
||||||
|
this.passwordIssue,
|
||||||
this.icCard,
|
this.icCard,
|
||||||
this.fingerprint,
|
this.fingerprint,
|
||||||
this.fingerVein,
|
this.fingerVein,
|
||||||
@ -374,10 +375,14 @@ class LockFeature {
|
|||||||
this.isNoSupportedBlueBroadcast,
|
this.isNoSupportedBlueBroadcast,
|
||||||
this.wifiLockType,
|
this.wifiLockType,
|
||||||
this.wifi,
|
this.wifi,
|
||||||
|
this.isH264,
|
||||||
|
this.isH265,
|
||||||
|
this.isMJpeg,
|
||||||
});
|
});
|
||||||
|
|
||||||
LockFeature.fromJson(Map<String, dynamic> json) {
|
LockFeature.fromJson(Map<String, dynamic> json) {
|
||||||
password = json['password'];
|
password = json['password'];
|
||||||
|
passwordIssue = json['passwordIssue'];
|
||||||
icCard = json['icCard'];
|
icCard = json['icCard'];
|
||||||
fingerprint = json['fingerprint'];
|
fingerprint = json['fingerprint'];
|
||||||
fingerVein = json['fingerVein'];
|
fingerVein = json['fingerVein'];
|
||||||
@ -391,9 +396,13 @@ class LockFeature {
|
|||||||
isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast'];
|
isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast'];
|
||||||
wifiLockType = json['wifiLockType'];
|
wifiLockType = json['wifiLockType'];
|
||||||
wifi = json['wifi'];
|
wifi = json['wifi'];
|
||||||
|
isH264 = json['isH264'];
|
||||||
|
isH265 = json['isH265'];
|
||||||
|
isMJpeg = json['isMJpeg'];
|
||||||
}
|
}
|
||||||
|
|
||||||
int? password;
|
int? password;
|
||||||
|
int? passwordIssue;
|
||||||
int? icCard;
|
int? icCard;
|
||||||
int? fingerprint;
|
int? fingerprint;
|
||||||
int? fingerVein;
|
int? fingerVein;
|
||||||
@ -407,10 +416,14 @@ class LockFeature {
|
|||||||
int? isNoSupportedBlueBroadcast;
|
int? isNoSupportedBlueBroadcast;
|
||||||
int? wifiLockType;
|
int? wifiLockType;
|
||||||
int? wifi;
|
int? wifi;
|
||||||
|
int? isH264;
|
||||||
|
int? isH265;
|
||||||
|
int? isMJpeg;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data['password'] = password;
|
data['password'] = password;
|
||||||
|
data['passwordIssue'] = passwordIssue;
|
||||||
data['icCard'] = icCard;
|
data['icCard'] = icCard;
|
||||||
data['fingerprint'] = fingerprint;
|
data['fingerprint'] = fingerprint;
|
||||||
data['fingerVein'] = fingerVein;
|
data['fingerVein'] = fingerVein;
|
||||||
@ -424,6 +437,9 @@ class LockFeature {
|
|||||||
data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast;
|
data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast;
|
||||||
data['wifiLockType'] = wifiLockType;
|
data['wifiLockType'] = wifiLockType;
|
||||||
data['wifi'] = wifi;
|
data['wifi'] = wifi;
|
||||||
|
data['isH264'] = isH264;
|
||||||
|
data['isH265'] = isH265;
|
||||||
|
data['isMJpeg'] = isMJpeg;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,13 +27,13 @@ class LockListLogic extends BaseGetXController {
|
|||||||
LockListLogic(this.entity) {}
|
LockListLogic(this.entity) {}
|
||||||
|
|
||||||
LockListState state = LockListState();
|
LockListState state = LockListState();
|
||||||
List<GroupList> _groupDataList = <GroupList>[];
|
final RxList<GroupList> groupDataList = <GroupList>[].obs;
|
||||||
LockListInfoGroupEntity? entity;
|
LockListInfoGroupEntity? entity;
|
||||||
final ShowTipView showTipView = ShowTipView();
|
final ShowTipView showTipView = ShowTipView();
|
||||||
|
|
||||||
List<GroupList> get groupDataList {
|
List<GroupList> get groupDataListFiltered {
|
||||||
final List<GroupList> list =
|
final List<GroupList> list =
|
||||||
_groupDataList.map((GroupList e) => e.copy()).toList();
|
groupDataList.map((GroupList e) => e.copy()).toList();
|
||||||
if (state.searchStr.value != '' && state.showSearch.value) {
|
if (state.searchStr.value != '' && state.showSearch.value) {
|
||||||
list.forEach((GroupList element) {
|
list.forEach((GroupList element) {
|
||||||
element.lockList?.removeWhere((LockListInfoItemEntity element) =>
|
element.lockList?.removeWhere((LockListInfoItemEntity element) =>
|
||||||
@ -60,15 +60,12 @@ class LockListLogic extends BaseGetXController {
|
|||||||
//设置数据
|
//设置数据
|
||||||
void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) {
|
void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) {
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
if (entity.pageNo == 1) {
|
groupDataList.value = entity.groupList!;
|
||||||
_groupDataList = <GroupList>[];
|
|
||||||
}
|
|
||||||
_groupDataList.addAll(entity.groupList!);
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听蓝牙协议返回结果
|
// 监听蓝牙协议返回结果
|
||||||
late StreamSubscription<Reply> _replySubscription;
|
late StreamSubscription<Reply> _replySubscription;
|
||||||
|
late StreamSubscription _setLockListInfoGroupEntity;
|
||||||
|
|
||||||
void _initReplySubscription() {
|
void _initReplySubscription() {
|
||||||
_replySubscription =
|
_replySubscription =
|
||||||
@ -336,17 +333,30 @@ class LockListLogic extends BaseGetXController {
|
|||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
_initReplySubscription();
|
_initReplySubscription();
|
||||||
|
_initEventHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// AppLog.log('onInit调用了 setLockListInfoGroupEntity');
|
AppLog.log('[onInit] entity: \\${entity?.toString()}');
|
||||||
setLockListInfoGroupEntity(entity!);
|
if (entity != null) {
|
||||||
|
setLockListInfoGroupEntity(entity!);
|
||||||
|
}
|
||||||
|
_initEventHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_replySubscription.cancel();
|
_replySubscription.cancel();
|
||||||
|
_setLockListInfoGroupEntity.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initEventHandler() {
|
||||||
|
_setLockListInfoGroupEntity = eventBus
|
||||||
|
.on<SetLockListInfoGroupEntity>()
|
||||||
|
.listen((SetLockListInfoGroupEntity event) async {
|
||||||
|
setLockListInfoGroupEntity(event.lockListInfoGroupEntity);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,32 +37,31 @@ class _LockListPageState extends State<LockListPage> with RouteAware {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<LockListLogic>(builder: (LockListLogic logic) {
|
return Obx(() => Scaffold(
|
||||||
return Scaffold(
|
body: ListView.separated(
|
||||||
body: ListView.separated(
|
itemCount: logic.groupDataListFiltered.length,
|
||||||
itemCount: logic.groupDataList.length,
|
itemBuilder: (BuildContext context, int index) {
|
||||||
itemBuilder: (BuildContext context, int index) {
|
final GroupList itemData = logic.groupDataListFiltered[index];
|
||||||
final GroupList itemData = logic.groupDataList[index];
|
return _buildLockExpandedList(context, index, itemData, key: ValueKey(itemData.groupId));
|
||||||
return _buildLockExpandedList(context, index, itemData);
|
},
|
||||||
},
|
shrinkWrap: true,
|
||||||
shrinkWrap: true,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
separatorBuilder: (BuildContext context, int index) {
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
return const Divider(
|
||||||
return const Divider(
|
height: 1,
|
||||||
height: 1,
|
color: AppColors.greyLineColor,
|
||||||
color: AppColors.greyLineColor,
|
);
|
||||||
);
|
}),
|
||||||
}),
|
));
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//设备多层级列表
|
//设备多层级列表
|
||||||
Widget _buildLockExpandedList(BuildContext context, int index,
|
Widget _buildLockExpandedList(BuildContext context, int index,
|
||||||
GroupList itemData) {
|
GroupList itemData, {Key? key}) {
|
||||||
final List<LockListInfoItemEntity> lockItemList =
|
final List<LockListInfoItemEntity> lockItemList =
|
||||||
itemData.lockList ?? <LockListInfoItemEntity>[];
|
itemData.lockList ?? <LockListInfoItemEntity>[];
|
||||||
return LockListGroupView(
|
return LockListGroupView(
|
||||||
|
key: key,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
//是否选中组
|
//是否选中组
|
||||||
if (itemData.isChecked) {} else {}
|
if (itemData.isChecked) {} else {}
|
||||||
|
|||||||
@ -132,20 +132,21 @@ class LockMainLogic extends BaseGetXController {
|
|||||||
state.lockListInfoGroupEntity.refresh();
|
state.lockListInfoGroupEntity.refresh();
|
||||||
// AppLog.log('entity:$entity state.lockListInfoGroupEntity.value.groupList!.length:${state.lockListInfoGroupEntity.value.groupList![0].lockList!.length}');
|
// AppLog.log('entity:$entity state.lockListInfoGroupEntity.value.groupList!.length:${state.lockListInfoGroupEntity.value.groupList![0].lockList!.length}');
|
||||||
//检测控制器是否存在
|
//检测控制器是否存在
|
||||||
if (Get.isRegistered<LockListLogic>()) {
|
eventBus.fire(SetLockListInfoGroupEntity(lockListInfoGroupEntity: entity));
|
||||||
//设置控制器数据并刷新
|
// if (Get.isRegistered<LockListLogic>()) {
|
||||||
// AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity');
|
// //设置控制器数据并刷新
|
||||||
Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
// // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity');
|
||||||
} else {
|
// Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||||
//延迟加载
|
// } else {
|
||||||
Future<dynamic>.delayed(200.milliseconds, () {
|
// //延迟加载
|
||||||
if (Get.isRegistered<LockListLogic>()) {
|
// Future<dynamic>.delayed(500.milliseconds, () {
|
||||||
//设置控制器数据并刷新
|
// if (Get.isRegistered<LockListLogic>()) {
|
||||||
// AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity');
|
// //设置控制器数据并刷新
|
||||||
Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
// // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity');
|
||||||
}
|
// Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||||
});
|
// }
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
if (state.dataLength.value == 1) {
|
if (state.dataLength.value == 1) {
|
||||||
if (Get.isRegistered<LockDetailLogic>()) {
|
if (Get.isRegistered<LockDetailLogic>()) {
|
||||||
|
|||||||
@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 获取手机联网token
|
// 获取手机联网token
|
||||||
Future<Response> getLockNetToken(String lockId) => post(
|
Future<Response> getLockNetToken(int lockId) => post(
|
||||||
getLockNetTokenURL.toUrl,
|
getLockNetTokenURL.toUrl,
|
||||||
jsonEncode({
|
jsonEncode({
|
||||||
'lockId': lockId,
|
'lockId': lockId,
|
||||||
|
|||||||
@ -325,7 +325,7 @@ class ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取手机联网token
|
// 获取手机联网token
|
||||||
Future<LockNetTokenEntity> getLockNetToken({required String lockId}) async {
|
Future<LockNetTokenEntity> getLockNetToken({required int lockId}) async {
|
||||||
final res = await apiProvider.getLockNetToken(lockId);
|
final res = await apiProvider.getLockNetToken(lockId);
|
||||||
return LockNetTokenEntity.fromJson(res.body);
|
return LockNetTokenEntity.fromJson(res.body);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,4 +16,8 @@ class TalkConstant {
|
|||||||
videoType: [VideoTypeE.H264],
|
videoType: [VideoTypeE.H264],
|
||||||
audioType: [AudioTypeE.G711],
|
audioType: [AudioTypeE.G711],
|
||||||
);
|
);
|
||||||
|
static TalkExpectReq H264_720P_Expect = TalkExpectReq(
|
||||||
|
videoType: [VideoTypeE.H264_720P],
|
||||||
|
audioType: [AudioTypeE.G711],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,18 @@ class ScpMessage {
|
|||||||
return 'ScpMessage{ProtocolFlag: $ProtocolFlag, MessageType: $MessageType, MessageId: $MessageId, SpTotal: $SpTotal, SpIndex: $SpIndex, FromPeerId: $FromPeerId, ToPeerId: $ToPeerId, PayloadType: $PayloadType, PayloadCRC: $PayloadCRC, PayloadLength: $PayloadLength, Payload: $Payload}';
|
return 'ScpMessage{ProtocolFlag: $ProtocolFlag, MessageType: $MessageType, MessageId: $MessageId, SpTotal: $SpTotal, SpIndex: $SpIndex, FromPeerId: $FromPeerId, ToPeerId: $ToPeerId, PayloadType: $PayloadType, PayloadCRC: $PayloadCRC, PayloadLength: $PayloadLength, Payload: $Payload}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:定长字符串编码
|
||||||
|
List<int> encodeFixedLengthString(String? str, int length) {
|
||||||
|
final bytes = utf8.encode(str ?? '');
|
||||||
|
if (bytes.length > length) {
|
||||||
|
return bytes.sublist(0, length);
|
||||||
|
} else if (bytes.length < length) {
|
||||||
|
return bytes + List.filled(length - bytes.length, 0);
|
||||||
|
} else {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String serialize() {
|
String serialize() {
|
||||||
final bytes = <int>[];
|
final bytes = <int>[];
|
||||||
|
|
||||||
@ -98,16 +110,19 @@ class ScpMessage {
|
|||||||
if (SpIndex != null) {
|
if (SpIndex != null) {
|
||||||
bytes.add(SpIndex!);
|
bytes.add(SpIndex!);
|
||||||
}
|
}
|
||||||
|
// FromPeerId (字符串,长度固定为44字节)
|
||||||
// FromPeerId (字符串,记录长度)
|
|
||||||
if (FromPeerId != null) {
|
if (FromPeerId != null) {
|
||||||
bytes.addAll(utf8.encode(FromPeerId!));
|
bytes.addAll(utf8.encode(FromPeerId!));
|
||||||
}
|
}
|
||||||
|
// FromPeerId (44字节定长)
|
||||||
|
// bytes.addAll(encodeFixedLengthString(FromPeerId, 44));
|
||||||
|
|
||||||
// ToPeerId (字符串,假设长度固定为32字节)
|
// ToPeerId (字符串,长度固定为44字节)
|
||||||
if (ToPeerId != null) {
|
if (ToPeerId != null) {
|
||||||
bytes.addAll(utf8.encode(ToPeerId!));
|
bytes.addAll(utf8.encode(ToPeerId!));
|
||||||
}
|
}
|
||||||
|
// ToPeerId (44字节定长)
|
||||||
|
// bytes.addAll(encodeFixedLengthString(ToPeerId, 44));
|
||||||
|
|
||||||
// PayloadType (2 bytes)
|
// PayloadType (2 bytes)
|
||||||
if (PayloadType != null) {
|
if (PayloadType != null) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
@ -22,7 +23,7 @@ class UdpEchoTestHandler extends ScpMessageBaseHandle
|
|||||||
EasyLoading.showToast(scpMessage.Payload, duration: 2000.milliseconds);
|
EasyLoading.showToast(scpMessage.Payload, duration: 2000.milliseconds);
|
||||||
} else {
|
} else {
|
||||||
talkDataRepository.addTalkData(
|
talkDataRepository.addTalkData(
|
||||||
TalkData(content: payload, contentType: TalkData_ContentTypeE.Image));
|
TalkDataModel(talkData: TalkData(content: payload, contentType: TalkData_ContentTypeE.Image)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
@ -13,6 +14,8 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart';
|
|||||||
import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
|
import 'package:star_lock/tools/storage.dart';
|
||||||
|
|
||||||
import '../../star_chart_manage.dart';
|
import '../../star_chart_manage.dart';
|
||||||
|
|
||||||
@ -32,7 +35,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
|
|||||||
// 停止同意接听的重发
|
// 停止同意接听的重发
|
||||||
startChartManage.stopTalkAcceptTimer();
|
startChartManage.stopTalkAcceptTimer();
|
||||||
// 接听之后增加期望音频的接收
|
// 接听之后增加期望音频的接收
|
||||||
_handleSendExpect();
|
_handleSendExpect(lockPeerID: scpMessage.FromPeerId!);
|
||||||
// 停止播放铃声
|
// 停止播放铃声
|
||||||
stopRingtone();
|
stopRingtone();
|
||||||
// 设置状态为接听成功
|
// 设置状态为接听成功
|
||||||
@ -76,8 +79,48 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSendExpect() {
|
/// 收到同意接听回复之后增加音频的期望数据
|
||||||
// 修改预期数据并启动发送预期数据定时器,在收到回复时停止
|
void _handleSendExpect({
|
||||||
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
required String lockPeerID,
|
||||||
|
}) async {
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
|
||||||
|
var isH264 = currentKeyInfo.lockFeature?.isH264 == 1;
|
||||||
|
var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
|
||||||
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
|
final lockList = element.lockList;
|
||||||
|
if (lockList != null && lockList.length != 0) {
|
||||||
|
for (var lockInfo in lockList) {
|
||||||
|
final peerId = lockInfo.network?.peerId;
|
||||||
|
if (peerId != null && peerId != '') {
|
||||||
|
if (peerId == lockPeerID) {
|
||||||
|
isH264 = lockInfo.lockFeature?.isH264 == 1;
|
||||||
|
isMJpeg = lockInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用H264,其次是MJPEG
|
||||||
|
if (isH264) {
|
||||||
|
// 锁支持H264,发送H264视频和G711音频期望
|
||||||
|
startChartManage.sendH264VideoAndG711AudioTalkExpectData();
|
||||||
|
print('锁支持H264,发送H264视频格式期望数据');
|
||||||
|
} else if (isMJpeg) {
|
||||||
|
// 锁只支持MJPEG,发送图像视频和G711音频期望
|
||||||
|
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
||||||
|
print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据');
|
||||||
|
} else {
|
||||||
|
// 默认使用图像视频
|
||||||
|
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
||||||
|
print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
|||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart';
|
import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
@ -61,9 +62,6 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
int? spTotal,
|
int? spTotal,
|
||||||
int? spIndex,
|
int? spIndex,
|
||||||
int? messageId}) {
|
int? messageId}) {
|
||||||
// 获取统计信息
|
|
||||||
final stats = PacketLossStatistics().getStatistics();
|
|
||||||
_asyncLog('丢包统计: $stats');
|
|
||||||
// _asyncLog(
|
// _asyncLog(
|
||||||
// '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength');
|
// '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength');
|
||||||
if (messageType == MessageTypeConstant.RealTimeData) {
|
if (messageType == MessageTypeConstant.RealTimeData) {
|
||||||
@ -118,7 +116,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
void _handleVideoH264(TalkData talkData) {
|
void _handleVideoH264(TalkData talkData) {
|
||||||
final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame();
|
final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame();
|
||||||
talkDataH264Frame.mergeFromBuffer(talkData.content);
|
talkDataH264Frame.mergeFromBuffer(talkData.content);
|
||||||
frameHandler.handleFrame(talkDataH264Frame);
|
frameHandler.handleFrame(talkDataH264Frame, talkData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理图片数据
|
/// 处理图片数据
|
||||||
@ -127,7 +125,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
await _processCompletePayload(Uint8List.fromList(talkData.content));
|
await _processCompletePayload(Uint8List.fromList(talkData.content));
|
||||||
processCompletePayload.forEach((element) {
|
processCompletePayload.forEach((element) {
|
||||||
talkData.content = element;
|
talkData.content = element;
|
||||||
talkDataRepository.addTalkData(talkData);
|
talkDataRepository.addTalkData(
|
||||||
|
TalkDataModel(
|
||||||
|
talkData: talkData,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +140,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
// // 转pcm数据
|
// // 转pcm数据
|
||||||
// List<int> pcmBytes = G711().convertList(g711Data);
|
// List<int> pcmBytes = G711().convertList(g711Data);
|
||||||
// talkData.content = pcmBytes;
|
// talkData.content = pcmBytes;
|
||||||
talkDataRepository.addTalkData(talkData);
|
talkDataRepository.addTalkData(
|
||||||
|
TalkDataModel(
|
||||||
|
talkData: talkData,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error decoding G.711 to PCM: $e');
|
print('Error decoding G.711 to PCM: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
@ -20,7 +21,7 @@ import '../../star_chart_manage.dart';
|
|||||||
|
|
||||||
class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
||||||
implements ScpMessageHandler {
|
implements ScpMessageHandler {
|
||||||
final TalkViewState talkViewState = Get.put(TalkViewLogic()).state;
|
// final TalkViewState talkViewState = Get.put(TalkViewLogic()).state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleReq(ScpMessage scpMessage) {
|
void handleReq(ScpMessage scpMessage) {
|
||||||
@ -40,7 +41,11 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
|||||||
startChartManage.stopTalkExpectMessageTimer();
|
startChartManage.stopTalkExpectMessageTimer();
|
||||||
// 停止发送对讲请求
|
// 停止发送对讲请求
|
||||||
startChartManage.stopCallRequestMessageTimer();
|
startChartManage.stopCallRequestMessageTimer();
|
||||||
talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0;
|
// talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0;
|
||||||
|
startChartManage.rotateAngle = talkExpectResp.rotate;
|
||||||
|
startChartManage.videoWidth = talkExpectResp.width;
|
||||||
|
startChartManage.videoHeight = talkExpectResp.height;
|
||||||
|
AppLog.log('视频画面需要旋转:${talkExpectResp.rotate},画面宽高:${talkExpectResp.width}-${talkExpectResp.height}');
|
||||||
// 收到预期数据的应答后,代表建立了连接,启动通话保持的监听
|
// 收到预期数据的应答后,代表建立了连接,启动通话保持的监听
|
||||||
// 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作);
|
// 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作);
|
||||||
talkePingOverTimeTimerManager.start();
|
talkePingOverTimeTimerManager.start();
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/appRouters.dart';
|
import 'package:star_lock/appRouters.dart';
|
||||||
import 'package:star_lock/app_settings/app_settings.dart';
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart';
|
|
||||||
import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart';
|
||||||
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
import 'package:star_lock/tools/push/xs_jPhush.dart';
|
import 'package:star_lock/tools/push/xs_jPhush.dart';
|
||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
import 'package:star_lock/translations/current_locale_tool.dart';
|
import 'package:star_lock/translations/current_locale_tool.dart';
|
||||||
@ -25,27 +23,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
|||||||
RxString currentLanguage =
|
RxString currentLanguage =
|
||||||
CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言
|
CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言
|
||||||
|
|
||||||
// 添加上次处理请求的时间戳
|
|
||||||
int _lastRequestTime = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleReq(ScpMessage scpMessage) async {
|
void handleReq(ScpMessage scpMessage) async {
|
||||||
|
|
||||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
// 确保与上次请求间隔至少1秒
|
|
||||||
if (currentTime - _lastRequestTime < 1000) {
|
|
||||||
// 如果间隔小于1秒,直接拒绝请求
|
|
||||||
replyErrorMessage(scpMessage);
|
|
||||||
AppLog.log('对讲请求过于频繁,已拒绝');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后处理时间
|
|
||||||
_lastRequestTime = currentTime;
|
|
||||||
|
|
||||||
// 判断是否登录账户
|
// 判断是否登录账户
|
||||||
final loginData = await Storage.getLoginData();
|
final loginData = await Storage.getLoginData();
|
||||||
|
|
||||||
// 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态
|
// 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态
|
||||||
if (loginData != null &&
|
if (loginData != null &&
|
||||||
(talkStatus.status != TalkStatus.passiveCallWaitingAnswer ||
|
(talkStatus.status != TalkStatus.passiveCallWaitingAnswer ||
|
||||||
@ -56,7 +37,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
|||||||
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
||||||
startChartManage.lockPeerId = scpMessage.FromPeerId!;
|
startChartManage.lockPeerId = scpMessage.FromPeerId!;
|
||||||
// 处理收到接听请求后的事件
|
// 处理收到接听请求后的事件
|
||||||
_talkRequestEvent(talkObjectName: talkReq.callerName);
|
_talkRequestEvent(
|
||||||
|
talkObjectName: talkReq.callerName,
|
||||||
|
lockPeerID: scpMessage.FromPeerId!,
|
||||||
|
);
|
||||||
|
|
||||||
// 回复成功
|
// 回复成功
|
||||||
replySuccessMessage(scpMessage);
|
replySuccessMessage(scpMessage);
|
||||||
@ -75,6 +59,11 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
|||||||
// 收到对讲请求的应答
|
// 收到对讲请求的应答
|
||||||
startChartManage.FromPeerId = scpMessage.ToPeerId!;
|
startChartManage.FromPeerId = scpMessage.ToPeerId!;
|
||||||
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
||||||
|
startChartManage.lockPeerId = scpMessage.FromPeerId!;
|
||||||
|
// 处理预期数据格式
|
||||||
|
_handleResponseSendExpect(
|
||||||
|
lockPeerID: scpMessage.FromPeerId!,
|
||||||
|
);
|
||||||
// 发送预期数据
|
// 发送预期数据
|
||||||
startChartManage.startTalkExpectTimer();
|
startChartManage.startTalkExpectTimer();
|
||||||
// 停止发送对讲请求
|
// 停止发送对讲请求
|
||||||
@ -95,28 +84,56 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
|||||||
void handleRealTimeData(ScpMessage scpMessage) {}
|
void handleRealTimeData(ScpMessage scpMessage) {}
|
||||||
|
|
||||||
// 来电事件的处理
|
// 来电事件的处理
|
||||||
void _talkRequestEvent({required String talkObjectName}) {
|
void _talkRequestEvent({
|
||||||
|
required String talkObjectName,
|
||||||
|
required String lockPeerID,
|
||||||
|
}) async {
|
||||||
// 发送预期数据、通知锁板需要获取视频数据
|
// 发送预期数据、通知锁板需要获取视频数据
|
||||||
_handleSendExpect();
|
_handleRequestSendExpect(lockPeerID: lockPeerID);
|
||||||
// 播放铃声
|
// 播放铃声
|
||||||
//test:使用自定义铃声
|
//test:使用自定义铃声
|
||||||
playRingtone();
|
playRingtone();
|
||||||
// 显示状态栏弹窗
|
// 显示状态栏弹窗
|
||||||
_showTalkRequestNotification(talkObjectName: talkObjectName);
|
// _showTalkRequestNotification(talkObjectName: talkObjectName);
|
||||||
// 设置为等待接听状态
|
// 设置为等待接听状态
|
||||||
talkStatus.setPassiveCallWaitingAnswer();
|
talkStatus.setPassiveCallWaitingAnswer();
|
||||||
// 收到呼叫请求,跳转到接听页面
|
|
||||||
|
// 获取锁支持项
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
var isWifiLockType = currentKeyInfo.lockFeature?.wifiLockType == 1;
|
||||||
|
|
||||||
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
|
final lockList = element.lockList;
|
||||||
|
if (lockList != null && lockList.length != 0) {
|
||||||
|
for (var lockInfo in lockList) {
|
||||||
|
final peerId = lockInfo.network?.peerId;
|
||||||
|
if (peerId != null && peerId != '') {
|
||||||
|
if (peerId == lockPeerID) {
|
||||||
|
isWifiLockType = lockInfo.lockFeature?.wifiLockType == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isWifiLockType) {
|
||||||
|
Get.toNamed(Routers.imageTransmissionView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (startChartManage
|
if (startChartManage
|
||||||
.getDefaultTalkExpect()
|
.getDefaultTalkExpect()
|
||||||
.videoType
|
.videoType
|
||||||
.indexOf(VideoTypeE.H264) ==
|
.contains(VideoTypeE.H264)) {
|
||||||
-1) {
|
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routers.starChartTalkView,
|
Routers.h264WebView,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routers.h264WebView,
|
Routers.starChartTalkView,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,8 +205,97 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSendExpect() {
|
/// app收到的对讲请求后,发送的预期数据
|
||||||
// 修改预期数据并启动发送预期数据定时器,在收到回复时停止
|
void _handleRequestSendExpect({
|
||||||
startChartManage.sendOnlyImageVideoTalkExpectData();
|
required String lockPeerID,
|
||||||
|
}) async {
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
|
||||||
|
var isH264 = currentKeyInfo.lockFeature?.isH264 == 1;
|
||||||
|
var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
|
||||||
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
|
final lockList = element.lockList;
|
||||||
|
if (lockList != null && lockList.length != 0) {
|
||||||
|
for (var lockInfo in lockList) {
|
||||||
|
final peerId = lockInfo.network?.peerId;
|
||||||
|
if (peerId != null && peerId != '') {
|
||||||
|
if (peerId == lockPeerID) {
|
||||||
|
isH264 = lockInfo.lockFeature?.isH264 == 1;
|
||||||
|
isMJpeg = lockInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 优先使用H264,其次是MJPEG
|
||||||
|
if (isH264) {
|
||||||
|
// 锁支持H264,发送H264视频和G711音频期望
|
||||||
|
startChartManage.sendOnlyH264VideoTalkExpectData();
|
||||||
|
print(
|
||||||
|
'app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
} else if (isMJpeg) {
|
||||||
|
// 锁只支持MJPEG,发送图像视频和G711音频期望
|
||||||
|
startChartManage.sendOnlyImageVideoTalkExpectData();
|
||||||
|
print(
|
||||||
|
'app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
} else {
|
||||||
|
// 默认使用图像视频
|
||||||
|
startChartManage.sendOnlyImageVideoTalkExpectData();
|
||||||
|
print(
|
||||||
|
'app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// app主动发请求,收到回复后发送的预期数据
|
||||||
|
void _handleResponseSendExpect({
|
||||||
|
required String lockPeerID,
|
||||||
|
}) async {
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
|
||||||
|
var isH264 = currentKeyInfo.lockFeature?.isH264 == 1;
|
||||||
|
var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
|
||||||
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
|
final lockList = element.lockList;
|
||||||
|
if (lockList != null && lockList.length != 0) {
|
||||||
|
for (var lockInfo in lockList) {
|
||||||
|
final peerId = lockInfo.network?.peerId;
|
||||||
|
if (peerId != null && peerId != '') {
|
||||||
|
if (peerId == lockPeerID) {
|
||||||
|
isH264 = lockInfo.lockFeature?.isH264 == 1;
|
||||||
|
isMJpeg = lockInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 优先使用H264,其次是MJPEG
|
||||||
|
if (isH264) {
|
||||||
|
// 锁支持H264,发送H264视频和G711音频期望
|
||||||
|
startChartManage.sendH264VideoAndG711AudioTalkExpectData();
|
||||||
|
AppLog.log(
|
||||||
|
'app主动发对讲请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
} else if (isMJpeg) {
|
||||||
|
// 锁只支持MJPEG,发送图像视频和G711音频期望
|
||||||
|
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
||||||
|
AppLog.log(
|
||||||
|
'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
} else {
|
||||||
|
// 默认使用图像视频
|
||||||
|
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
||||||
|
AppLog.log(
|
||||||
|
'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,17 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:star_lock/app_settings/app_settings.dart';
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
import '../../proto/talk_data_h264_frame.pb.dart';
|
import '../../proto/talk_data_h264_frame.pb.dart';
|
||||||
|
|
||||||
class H264FrameHandler {
|
class H264FrameHandler {
|
||||||
|
final void Function(TalkDataModel frameData) onCompleteFrame;
|
||||||
final void Function(List<int> frameData) onCompleteFrame;
|
|
||||||
|
|
||||||
H264FrameHandler({required this.onCompleteFrame});
|
H264FrameHandler({required this.onCompleteFrame});
|
||||||
|
|
||||||
void handleFrame(TalkDataH264Frame frame) {
|
void handleFrame(TalkDataH264Frame frame, TalkData talkData) {
|
||||||
onCompleteFrame(frame.frameData);
|
onCompleteFrame(
|
||||||
|
TalkDataModel(talkData: talkData, talkDataH264Frame: frame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,10 @@ class PacketLossStatistics {
|
|||||||
// key: messageId, value: {totalPackets, receivedPackets}
|
// key: messageId, value: {totalPackets, receivedPackets}
|
||||||
final Map<int, PacketInfo> _packetsMap = HashMap();
|
final Map<int, PacketInfo> _packetsMap = HashMap();
|
||||||
|
|
||||||
|
// 配置参数
|
||||||
|
int _maxCapacity = 300; // 最大容量为300条记录
|
||||||
|
int _timeoutMs = 30000; // 默认超时时间为30秒
|
||||||
|
|
||||||
// 统计信息
|
// 统计信息
|
||||||
int _totalMessages = 0; // 总消息数
|
int _totalMessages = 0; // 总消息数
|
||||||
int _lostMessages = 0; // 丢包的消息数
|
int _lostMessages = 0; // 丢包的消息数
|
||||||
@ -18,10 +22,19 @@ class PacketLossStatistics {
|
|||||||
|
|
||||||
// 记录分包数据
|
// 记录分包数据
|
||||||
void recordPacket(int messageId, int currentIndex, int totalPackets) {
|
void recordPacket(int messageId, int currentIndex, int totalPackets) {
|
||||||
|
// 定期清理超时记录
|
||||||
|
_cleanupExpiredPackets();
|
||||||
|
|
||||||
|
// 检查容量限制
|
||||||
|
_checkCapacityLimit();
|
||||||
|
|
||||||
if (!_packetsMap.containsKey(messageId)) {
|
if (!_packetsMap.containsKey(messageId)) {
|
||||||
_packetsMap[messageId] = PacketInfo(totalPackets);
|
_packetsMap[messageId] = PacketInfo(totalPackets);
|
||||||
_totalMessages++;
|
_totalMessages++;
|
||||||
_totalPackets += totalPackets;
|
_totalPackets += totalPackets;
|
||||||
|
} else {
|
||||||
|
// 更新时间戳
|
||||||
|
_packetsMap[messageId]!.timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
_packetsMap[messageId]!.receivedPackets.add(currentIndex);
|
_packetsMap[messageId]!.receivedPackets.add(currentIndex);
|
||||||
@ -32,6 +45,51 @@ class PacketLossStatistics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理超时的记录
|
||||||
|
void _cleanupExpiredPackets() {
|
||||||
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final expiredMessageIds = <int>[];
|
||||||
|
|
||||||
|
_packetsMap.forEach((messageId, info) {
|
||||||
|
// 如果记录超时,添加到待清理列表
|
||||||
|
if (currentTime - info.timestamp > _timeoutMs) {
|
||||||
|
expiredMessageIds.add(messageId);
|
||||||
|
|
||||||
|
// 统计丢包
|
||||||
|
_lostMessages++;
|
||||||
|
_lostPackets += (info.totalPackets - info.receivedPackets.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除超时记录
|
||||||
|
for (var messageId in expiredMessageIds) {
|
||||||
|
_packetsMap.remove(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并确保不超过最大容量
|
||||||
|
void _checkCapacityLimit() {
|
||||||
|
if (_packetsMap.length <= _maxCapacity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果超过容量限制,按时间戳排序并删除最旧的记录
|
||||||
|
var entries = _packetsMap.entries.toList()
|
||||||
|
..sort((a, b) => a.value.timestamp.compareTo(b.value.timestamp));
|
||||||
|
|
||||||
|
// 计算需要移除的数量(移除25%的旧记录,至少保证有一定空间)
|
||||||
|
int removeCount = (_packetsMap.length * 0.25).ceil();
|
||||||
|
|
||||||
|
// 移除并统计丢包
|
||||||
|
for (int i = 0; i < removeCount && i < entries.length; i++) {
|
||||||
|
var entry = entries[i];
|
||||||
|
_lostMessages++;
|
||||||
|
_lostPackets +=
|
||||||
|
(entry.value.totalPackets - entry.value.receivedPackets.length);
|
||||||
|
_packetsMap.remove(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查丢包情况
|
// 检查丢包情况
|
||||||
void _checkPacketLoss(int messageId) {
|
void _checkPacketLoss(int messageId) {
|
||||||
final info = _packetsMap[messageId]!;
|
final info = _packetsMap[messageId]!;
|
||||||
@ -62,6 +120,28 @@ class PacketLossStatistics {
|
|||||||
return PacketLossInfo(messageLossRate, packetLossRate);
|
return PacketLossInfo(messageLossRate, packetLossRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Getter和Setter,允许外部调整参数
|
||||||
|
int get maxCapacity => _maxCapacity;
|
||||||
|
set maxCapacity(int value) {
|
||||||
|
if (value > 0) {
|
||||||
|
_maxCapacity = value;
|
||||||
|
// 设置新容量后立即检查
|
||||||
|
_checkCapacityLimit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get timeoutMs => _timeoutMs;
|
||||||
|
set timeoutMs(int value) {
|
||||||
|
if (value > 0) {
|
||||||
|
_timeoutMs = value;
|
||||||
|
// 设置新超时后立即清理
|
||||||
|
_cleanupExpiredPackets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前未完成记录数
|
||||||
|
int get pendingRecordsCount => _packetsMap.length;
|
||||||
|
|
||||||
// 重置统计数据
|
// 重置统计数据
|
||||||
void reset() {
|
void reset() {
|
||||||
_packetsMap.clear();
|
_packetsMap.clear();
|
||||||
@ -76,8 +156,10 @@ class PacketLossStatistics {
|
|||||||
class PacketInfo {
|
class PacketInfo {
|
||||||
final int totalPackets;
|
final int totalPackets;
|
||||||
final Set<int> receivedPackets = HashSet<int>();
|
final Set<int> receivedPackets = HashSet<int>();
|
||||||
|
int timestamp; // 添加时间戳字段,记录最后更新时间
|
||||||
|
|
||||||
PacketInfo(this.totalPackets);
|
PacketInfo(this.totalPackets)
|
||||||
|
: timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 丢包统计信息类
|
// 丢包统计信息类
|
||||||
|
|||||||
9
lib/talk/starChart/handle/other/talk_data_model.dart
Normal file
9
lib/talk/starChart/handle/other/talk_data_model.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
|
||||||
|
|
||||||
|
class TalkDataModel {
|
||||||
|
TalkData? talkData;
|
||||||
|
TalkDataH264Frame? talkDataH264Frame;
|
||||||
|
|
||||||
|
TalkDataModel({required this.talkData, this.talkDataH264Frame});
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
|
|
||||||
class TalkDataRepository {
|
class TalkDataRepository {
|
||||||
TalkDataRepository._() {
|
TalkDataRepository._() {
|
||||||
_talkDataStreamController = StreamController<TalkData>.broadcast(
|
_talkDataStreamController = StreamController<TalkDataModel>.broadcast(
|
||||||
onListen: () {
|
onListen: () {
|
||||||
_isListening = true;
|
_isListening = true;
|
||||||
},
|
},
|
||||||
@ -18,13 +19,13 @@ class TalkDataRepository {
|
|||||||
|
|
||||||
static TalkDataRepository get instance => _instance;
|
static TalkDataRepository get instance => _instance;
|
||||||
|
|
||||||
late final StreamController<TalkData> _talkDataStreamController;
|
late final StreamController<TalkDataModel> _talkDataStreamController;
|
||||||
bool _isListening = false;
|
bool _isListening = false;
|
||||||
|
|
||||||
// 直接返回原始流,不做转换
|
// 直接返回原始流,不做转换
|
||||||
Stream<TalkData> get talkDataStream => _talkDataStreamController.stream;
|
Stream<TalkDataModel> get talkDataStream => _talkDataStreamController.stream;
|
||||||
|
|
||||||
void addTalkData(TalkData talkData) {
|
void addTalkData(TalkDataModel talkData) {
|
||||||
if (_isListening) {
|
if (_isListening) {
|
||||||
_talkDataStreamController.add(talkData);
|
_talkDataStreamController.add(talkData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import 'package:star_lock/talk/starChart/constant/payload_type_constant.dart';
|
|||||||
import 'package:star_lock/talk/starChart/constant/udp_constant.dart';
|
import 'package:star_lock/talk/starChart/constant/udp_constant.dart';
|
||||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart';
|
import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
|
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart';
|
||||||
@ -55,10 +56,10 @@ class ScpMessageBaseHandle {
|
|||||||
|
|
||||||
// 处理出完整帧数据后的回调
|
// 处理出完整帧数据后的回调
|
||||||
final H264FrameHandler frameHandler =
|
final H264FrameHandler frameHandler =
|
||||||
H264FrameHandler(onCompleteFrame: (frameData) {
|
H264FrameHandler(onCompleteFrame: (TalkDataModel talkDataModel) {
|
||||||
// 处理完整的帧数据
|
// 处理完整的帧数据
|
||||||
TalkDataRepository.instance.addTalkData(
|
TalkDataRepository.instance.addTalkData(
|
||||||
TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData),
|
talkDataModel,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ class ScpMessageBaseHandle {
|
|||||||
messageId: scpMessage.MessageId!,
|
messageId: scpMessage.MessageId!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回复失败消息
|
// 回复失败消息
|
||||||
void replyErrorMessage(ScpMessage scpMessage) {
|
void replyErrorMessage(ScpMessage scpMessage) {
|
||||||
startChartManage.sendGenericRespErrorMessage(
|
startChartManage.sendGenericRespErrorMessage(
|
||||||
|
|||||||
@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum {
|
|||||||
static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264');
|
static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264');
|
||||||
static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE');
|
static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE');
|
||||||
static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8');
|
static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8');
|
||||||
|
static const VideoTypeE H264_720P = VideoTypeE._(4, _omitEnumNames ? '' : 'H264_720P');
|
||||||
|
|
||||||
static const $core.List<VideoTypeE> values = <VideoTypeE> [
|
static const $core.List<VideoTypeE> values = <VideoTypeE> [
|
||||||
NONE_V,
|
NONE_V,
|
||||||
H264,
|
H264,
|
||||||
IMAGE,
|
IMAGE,
|
||||||
VP8,
|
VP8,
|
||||||
|
H264_720P,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values);
|
static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
|||||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pbserver.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pbserver.dart';
|
||||||
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
||||||
import 'package:star_lock/tools/baseGetXController.dart';
|
import 'package:star_lock/tools/baseGetXController.dart';
|
||||||
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
import 'package:star_lock/tools/deviceInfo_utils.dart';
|
import 'package:star_lock/tools/deviceInfo_utils.dart';
|
||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@ -112,8 +113,12 @@ class StartChartManage {
|
|||||||
RbcuConfirm? rbcuConfirm;
|
RbcuConfirm? rbcuConfirm;
|
||||||
final int _maxPayloadSize = 8 * 1024; // 分包大小
|
final int _maxPayloadSize = 8 * 1024; // 分包大小
|
||||||
|
|
||||||
|
int rotateAngle = 0; // 视频旋转角度
|
||||||
|
int videoWidth = 0; // 视频宽度
|
||||||
|
int videoHeight = 0; // 视频高度
|
||||||
|
|
||||||
// 默认通话的期望数据格式
|
// 默认通话的期望数据格式
|
||||||
TalkExpectReq _defaultTalkExpect = TalkConstant.ImageExpect;
|
TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect;
|
||||||
|
|
||||||
String relayPeerId = ''; // 中继peerId
|
String relayPeerId = ''; // 中继peerId
|
||||||
|
|
||||||
@ -227,20 +232,6 @@ class StartChartManage {
|
|||||||
|
|
||||||
/// 设置数据接收回调
|
/// 设置数据接收回调
|
||||||
_onReceiveData(_udpSocket!, Get.context!);
|
_onReceiveData(_udpSocket!, Get.context!);
|
||||||
|
|
||||||
// //ToDo: 增加对讲调试、正式可删除
|
|
||||||
// // 每秒重置数据速率
|
|
||||||
// Timer.periodic(Duration(seconds: 1), (Timer t) {
|
|
||||||
// UdpTalkDataHandler().resetDataRates();
|
|
||||||
// // 更新调试信息
|
|
||||||
// Provider.of<DebugInfoModel>(Get.context!, listen: false)
|
|
||||||
// .updateDebugInfo(
|
|
||||||
// UdpTalkDataHandler().getLastRecvDataRate() ~/ 1024, // 转换为KB
|
|
||||||
// UdpTalkDataHandler().getLastRecvPacketCount(),
|
|
||||||
// UdpTalkDataHandler().getLastSendDataRate() ~/ 1024, // 转换为KB
|
|
||||||
// UdpTalkDataHandler().getLastSendPacketCount(),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
_log(text: 'Failed to bind UDP socket: $error');
|
_log(text: 'Failed to bind UDP socket: $error');
|
||||||
});
|
});
|
||||||
@ -419,17 +410,36 @@ class StartChartManage {
|
|||||||
/// 启动持续发送对讲请求
|
/// 启动持续发送对讲请求
|
||||||
void startCallRequestMessageTimer({required String ToPeerId}) async {
|
void startCallRequestMessageTimer({required String ToPeerId}) async {
|
||||||
// 如果已经处于等待接听状态就不发送
|
// 如果已经处于等待接听状态就不发送
|
||||||
if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
// if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
||||||
// 如果是h264则跳转至webview
|
// // 如果是h264则跳转至webview
|
||||||
if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) {
|
// if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) {
|
||||||
Get.toNamed(
|
// Get.toNamed(
|
||||||
Routers.h264WebView,
|
// Routers.h264WebView,
|
||||||
);
|
// );
|
||||||
} else {
|
// } else {
|
||||||
Get.toNamed(
|
// Get.toNamed(
|
||||||
Routers.starChartTalkView,
|
// Routers.starChartTalkView,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
final isH264 = currentKeyInfo.lockFeature?.isH264 == 1;
|
||||||
|
final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1;
|
||||||
|
|
||||||
|
// 优先使用H264,其次是MJPEG
|
||||||
|
if (isH264) {
|
||||||
|
Get.toNamed(
|
||||||
|
Routers.h264WebView,
|
||||||
|
);
|
||||||
|
} else if (isMJpeg) {
|
||||||
|
Get.toNamed(
|
||||||
|
Routers.starChartTalkView,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.toNamed(
|
||||||
|
Routers.starChartTalkView,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 启动定时器持续发送对讲请求
|
// 启动定时器持续发送对讲请求
|
||||||
talkRequestTimer ??= Timer.periodic(
|
talkRequestTimer ??= Timer.periodic(
|
||||||
@ -596,7 +606,7 @@ class StartChartManage {
|
|||||||
void startTalkRejectMessageTimer() async {
|
void startTalkRejectMessageTimer() async {
|
||||||
try {
|
try {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
final int maxCount = 10; // 最大执行次数为10秒
|
final int maxCount = 3; // 最大执行次数为10秒
|
||||||
|
|
||||||
talkRejectTimer ??= Timer.periodic(
|
talkRejectTimer ??= Timer.periodic(
|
||||||
Duration(seconds: _defaultIntervalTime),
|
Duration(seconds: _defaultIntervalTime),
|
||||||
@ -622,6 +632,8 @@ class StartChartManage {
|
|||||||
stopCallRequestMessageTimer();
|
stopCallRequestMessageTimer();
|
||||||
stopSendingRbcuInfoMessages();
|
stopSendingRbcuInfoMessages();
|
||||||
stopSendingRbcuProBeMessages();
|
stopSendingRbcuProBeMessages();
|
||||||
|
stopTalkAcceptTimer();
|
||||||
|
stopCallRequestMessageTimer();
|
||||||
// 取消定时器
|
// 取消定时器
|
||||||
|
|
||||||
talkePingOverTimeTimerManager.cancel();
|
talkePingOverTimeTimerManager.cancel();
|
||||||
@ -720,6 +732,8 @@ class StartChartManage {
|
|||||||
stopCallRequestMessageTimer();
|
stopCallRequestMessageTimer();
|
||||||
stopSendingRbcuInfoMessages();
|
stopSendingRbcuInfoMessages();
|
||||||
stopSendingRbcuProBeMessages();
|
stopSendingRbcuProBeMessages();
|
||||||
|
stopTalkAcceptTimer();
|
||||||
|
stopCallRequestMessageTimer();
|
||||||
// 取消定时器
|
// 取消定时器
|
||||||
talkePingOverTimeTimerManager.cancel();
|
talkePingOverTimeTimerManager.cancel();
|
||||||
talkDataOverTimeTimerManager.cancel();
|
talkDataOverTimeTimerManager.cancel();
|
||||||
@ -1145,7 +1159,7 @@ class StartChartManage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reSetDefaultTalkExpect() {
|
void reSetDefaultTalkExpect() {
|
||||||
_defaultTalkExpect = TalkConstant.ImageExpect;
|
_defaultTalkExpect = TalkConstant.H264Expect;
|
||||||
}
|
}
|
||||||
|
|
||||||
TalkExpectReq getDefaultTalkExpect() {
|
TalkExpectReq getDefaultTalkExpect() {
|
||||||
@ -1163,12 +1177,27 @@ class StartChartManage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 修改预期接收到的数据
|
/// 修改预期接收到的数据
|
||||||
void sendImageVideoAndG711AudioTalkExpectData() {
|
void sendOnlyH264VideoTalkExpectData() {
|
||||||
final talkExpectReq = TalkConstant.ImageExpect;
|
final talkExpectReq = TalkExpectReq(
|
||||||
|
videoType: [VideoTypeE.H264],
|
||||||
|
audioType: [],
|
||||||
|
);
|
||||||
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||||
talkExpect: talkExpectReq);
|
talkExpect: talkExpectReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 修改预期接收到的数据
|
||||||
|
void sendImageVideoAndG711AudioTalkExpectData() {
|
||||||
|
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||||
|
talkExpect: TalkConstant.ImageExpect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改预期接收到的数据
|
||||||
|
void sendH264VideoAndG711AudioTalkExpectData() {
|
||||||
|
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||||
|
talkExpect: TalkConstant.H264Expect);
|
||||||
|
}
|
||||||
|
|
||||||
/// 发送远程开锁
|
/// 发送远程开锁
|
||||||
void sendRemoteUnLockMessage({
|
void sendRemoteUnLockMessage({
|
||||||
required String bluetoothDeviceName,
|
required String bluetoothDeviceName,
|
||||||
@ -1198,6 +1227,16 @@ class StartChartManage {
|
|||||||
|
|
||||||
/// 销毁资源
|
/// 销毁资源
|
||||||
void destruction() async {
|
void destruction() async {
|
||||||
|
// 先挂断
|
||||||
|
final status = talkStatus.status;
|
||||||
|
if (status == TalkStatus.passiveCallWaitingAnswer ||
|
||||||
|
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
||||||
|
status == TalkStatus.answeredSuccessfully ||
|
||||||
|
status == TalkStatus.uninitialized) {
|
||||||
|
startTalkRejectMessageTimer();
|
||||||
|
startTalkHangupMessageTimer();
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
}
|
||||||
isOnlineStarChartServer = false;
|
isOnlineStarChartServer = false;
|
||||||
// 停止发送心跳消息
|
// 停止发送心跳消息
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
@ -1225,7 +1264,6 @@ class StartChartManage {
|
|||||||
await Storage.removerStarChartRegisterNodeInfo();
|
await Storage.removerStarChartRegisterNodeInfo();
|
||||||
// 关闭udp服务
|
// 关闭udp服务
|
||||||
closeUdpSocket();
|
closeUdpSocket();
|
||||||
PacketLossStatistics().reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重置数据
|
/// 重置数据
|
||||||
|
|||||||
@ -30,14 +30,13 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
|
|||||||
// 处理应用程序进入后台的逻辑
|
// 处理应用程序进入后台的逻辑
|
||||||
|
|
||||||
final status = StartChartManage().talkStatus.status;
|
final status = StartChartManage().talkStatus.status;
|
||||||
|
|
||||||
if (status == TalkStatus.passiveCallWaitingAnswer ||
|
if (status == TalkStatus.passiveCallWaitingAnswer ||
|
||||||
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
||||||
status == TalkStatus.answeredSuccessfully ||
|
status == TalkStatus.answeredSuccessfully ||
|
||||||
status == TalkStatus.uninitialized) {
|
status == TalkStatus.uninitialized) {
|
||||||
StartChartManage().destruction();
|
|
||||||
Get.back();
|
Get.back();
|
||||||
}
|
}
|
||||||
|
StartChartManage().destruction();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAppResumed() async {
|
void onAppResumed() async {
|
||||||
|
|||||||
@ -0,0 +1,675 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:math'; // Import the math package to use sqrt
|
||||||
|
import 'dart:ui' show decodeImageFromList;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||||
|
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||||
|
import 'package:gallery_saver/gallery_saver.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||||||
|
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
|
||||||
|
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
|
||||||
|
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
|
||||||
|
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
||||||
|
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
|
import 'package:star_lock/network/api_repository.dart';
|
||||||
|
import 'package:star_lock/talk/call/g711.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_state.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
|
import 'package:star_lock/tools/G711Tool.dart';
|
||||||
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
|
import 'package:star_lock/tools/storage.dart';
|
||||||
|
|
||||||
|
import '../../../../tools/baseGetXController.dart';
|
||||||
|
|
||||||
|
class ImageTransmissionLogic extends BaseGetXController {
|
||||||
|
ImageTransmissionState state = ImageTransmissionState();
|
||||||
|
|
||||||
|
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
|
||||||
|
|
||||||
|
int bufferSize = 8; // 增大缓冲区,满时才渲染
|
||||||
|
|
||||||
|
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||||
|
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||||
|
|
||||||
|
int _startAudioTime = 0; // 开始播放时间戳
|
||||||
|
|
||||||
|
// 定义音频帧缓冲和发送函数
|
||||||
|
final List<int> _bufferedAudioFrames = <int>[];
|
||||||
|
|
||||||
|
// 添加监听状态和订阅引用
|
||||||
|
bool _isListening = false;
|
||||||
|
StreamSubscription? _streamSubscription;
|
||||||
|
|
||||||
|
Timer? videoRenderTimer; // 视频渲染定时器
|
||||||
|
|
||||||
|
int _renderedFrameCount = 0;
|
||||||
|
int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
/// 初始化音频播放器
|
||||||
|
void _initFlutterPcmSound() {
|
||||||
|
const int sampleRate = 8000;
|
||||||
|
FlutterPcmSound.setLogLevel(LogLevel.none);
|
||||||
|
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
||||||
|
// 设置 feed 阈值
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
||||||
|
} else {
|
||||||
|
FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 挂断
|
||||||
|
void udpHangUpAction() async {
|
||||||
|
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||||
|
// 如果是通话中就挂断
|
||||||
|
StartChartManage().startTalkHangupMessageTimer();
|
||||||
|
} else {
|
||||||
|
// 拒绝
|
||||||
|
StartChartManage().startTalkRejectMessageTimer();
|
||||||
|
}
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起接听命令
|
||||||
|
void initiateAnswerCommand() {
|
||||||
|
StartChartManage().startTalkAcceptTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听音视频数据流
|
||||||
|
void _startListenTalkData() {
|
||||||
|
// 防止重复监听
|
||||||
|
if (_isListening) {
|
||||||
|
AppLog.log("已经存在数据流监听,避免重复监听");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLog.log("==== 启动新的数据流监听 ====");
|
||||||
|
_isListening = true;
|
||||||
|
_streamSubscription = state.talkDataRepository.talkDataStream
|
||||||
|
.listen((TalkDataModel talkDataModel) async {
|
||||||
|
final talkData = talkDataModel.talkData;
|
||||||
|
final contentType = talkData!.contentType;
|
||||||
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 判断数据类型,进行分发处理
|
||||||
|
switch (contentType) {
|
||||||
|
case TalkData_ContentTypeE.G711:
|
||||||
|
// // 第一帧到达时记录开始时间
|
||||||
|
if (_isFirstAudioFrame) {
|
||||||
|
_startAudioTime = currentTime;
|
||||||
|
_isFirstAudioFrame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音频延迟
|
||||||
|
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||||
|
final audioDelay = currentTime - expectedTime;
|
||||||
|
|
||||||
|
// 如果延迟太大,清空缓冲区并直接播放
|
||||||
|
if (audioDelay > 500) {
|
||||||
|
state.audioBuffer.clear();
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
_playAudioFrames();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.audioBuffer.length >= audioBufferSize) {
|
||||||
|
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||||
|
}
|
||||||
|
state.audioBuffer.add(talkData); // 添加新数据
|
||||||
|
// 添加音频播放逻辑,与视频类似
|
||||||
|
_playAudioFrames();
|
||||||
|
break;
|
||||||
|
case TalkData_ContentTypeE.Image:
|
||||||
|
// 固定长度缓冲区,最多保留bufferSize帧
|
||||||
|
state.videoBuffer.add(talkData);
|
||||||
|
if (state.videoBuffer.length > bufferSize) {
|
||||||
|
state.videoBuffer.removeAt(0); // 移除最旧帧
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:音频帧播放逻辑
|
||||||
|
void _playAudioFrames() {
|
||||||
|
// 如果缓冲区为空或未达到目标大小,不进行播放
|
||||||
|
// 音频缓冲区要求更小,以减少延迟
|
||||||
|
if (state.audioBuffer.isEmpty ||
|
||||||
|
state.audioBuffer.length < audioBufferSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找出时间戳最小的音频帧
|
||||||
|
TalkData? oldestFrame;
|
||||||
|
int oldestIndex = -1;
|
||||||
|
for (int i = 0; i < state.audioBuffer.length; i++) {
|
||||||
|
if (oldestFrame == null ||
|
||||||
|
state.audioBuffer[i].durationMs < oldestFrame.durationMs) {
|
||||||
|
oldestFrame = state.audioBuffer[i];
|
||||||
|
oldestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保找到了有效帧
|
||||||
|
if (oldestFrame != null && oldestIndex != -1) {
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
// 播放音频
|
||||||
|
_playAudioData(oldestFrame);
|
||||||
|
}
|
||||||
|
state.audioBuffer.removeAt(oldestIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 监听对讲状态
|
||||||
|
void _startListenTalkStatus() {
|
||||||
|
state.startChartTalkStatus.statusStream.listen((talkStatus) {
|
||||||
|
state.talkStatus.value = talkStatus;
|
||||||
|
switch (talkStatus) {
|
||||||
|
case TalkStatus.rejected:
|
||||||
|
case TalkStatus.hangingUpDuring:
|
||||||
|
case TalkStatus.notTalkData:
|
||||||
|
case TalkStatus.notTalkPing:
|
||||||
|
case TalkStatus.end:
|
||||||
|
_handleInvalidTalkStatus();
|
||||||
|
break;
|
||||||
|
case TalkStatus.answeredSuccessfully:
|
||||||
|
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||||
|
state.oneMinuteTimeTimer ??=
|
||||||
|
Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||||
|
if (state.listData.value.length > 0) {
|
||||||
|
state.oneMinuteTime.value++;
|
||||||
|
// if (state.oneMinuteTime.value >= 60) {
|
||||||
|
// t.cancel(); // 取消定时器
|
||||||
|
// state.oneMinuteTime.value = 0;
|
||||||
|
// // 倒计时结束挂断
|
||||||
|
// // udpHangUpAction();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 其他状态的处理
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 播放音频数据
|
||||||
|
void _playAudioData(TalkData talkData) async {
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
final list =
|
||||||
|
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
|
||||||
|
// // 将 PCM 数据转换为 PcmArrayInt16
|
||||||
|
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
||||||
|
FlutterPcmSound.feed(fromList);
|
||||||
|
if (!state.isPlaying.value) {
|
||||||
|
FlutterPcmSound.play();
|
||||||
|
state.isPlaying.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止播放音频
|
||||||
|
void _stopPlayG711Data() async {
|
||||||
|
await FlutterPcmSound.pause();
|
||||||
|
await FlutterPcmSound.stop();
|
||||||
|
await FlutterPcmSound.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开门
|
||||||
|
// udpOpenDoorAction() async {
|
||||||
|
// final List<String>? privateKey =
|
||||||
|
// await Storage.getStringList(saveBluePrivateKey);
|
||||||
|
// final List<int> getPrivateKeyList = changeStringListToIntList(privateKey!);
|
||||||
|
//
|
||||||
|
// final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
|
||||||
|
// final List<int> signKeyDataList = changeStringListToIntList(signKey!);
|
||||||
|
//
|
||||||
|
// final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||||
|
// final List<int> getTokenList = changeStringListToIntList(token!);
|
||||||
|
//
|
||||||
|
// await _getLockNetToken();
|
||||||
|
//
|
||||||
|
// final OpenLockCommand openLockCommand = OpenLockCommand(
|
||||||
|
// lockID: BlueManage().connectDeviceName,
|
||||||
|
// userID: await Storage.getUid(),
|
||||||
|
// openMode: lockDetailState.openDoorModel,
|
||||||
|
// openTime: _getUTCNetTime(),
|
||||||
|
// onlineToken: lockDetailState.lockNetToken,
|
||||||
|
// token: getTokenList,
|
||||||
|
// needAuthor: 1,
|
||||||
|
// signKey: signKeyDataList,
|
||||||
|
// privateKey: getPrivateKeyList,
|
||||||
|
// );
|
||||||
|
// final messageDetail = openLockCommand.packageData();
|
||||||
|
// // 将 List<int> 转换为十六进制字符串
|
||||||
|
// String hexString = messageDetail
|
||||||
|
// .map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
// .join(' ');
|
||||||
|
//
|
||||||
|
// AppLog.log('open lock hexString: $hexString');
|
||||||
|
// // 发送远程开门消息
|
||||||
|
// StartChartManage().sendRemoteUnLockMessage(
|
||||||
|
// bluetoothDeviceName: BlueManage().connectDeviceName,
|
||||||
|
// openLockCommand: messageDetail,
|
||||||
|
// );
|
||||||
|
// showToast('正在开锁中...'.tr);
|
||||||
|
// }
|
||||||
|
|
||||||
|
int _getUTCNetTime() {
|
||||||
|
if (lockDetailState.isHaveNetwork) {
|
||||||
|
return DateTime.now().millisecondsSinceEpoch ~/ 1000 +
|
||||||
|
lockDetailState.differentialTime;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取权限状态
|
||||||
|
Future<bool> getPermissionStatus() async {
|
||||||
|
final Permission permission = Permission.microphone;
|
||||||
|
//granted 通过,denied 被拒绝,permanentlyDenied 拒绝且不在提示
|
||||||
|
final PermissionStatus status = await permission.status;
|
||||||
|
if (status.isGranted) {
|
||||||
|
return true;
|
||||||
|
} else if (status.isDenied) {
|
||||||
|
requestPermission(permission);
|
||||||
|
} else if (status.isPermanentlyDenied) {
|
||||||
|
openAppSettings();
|
||||||
|
} else if (status.isRestricted) {
|
||||||
|
requestPermission(permission);
|
||||||
|
} else {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
///申请权限
|
||||||
|
void requestPermission(Permission permission) async {
|
||||||
|
final PermissionStatus status = await permission.request();
|
||||||
|
if (status.isPermanentlyDenied) {
|
||||||
|
openAppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestPermissions() async {
|
||||||
|
// 申请存储权限
|
||||||
|
var storageStatus = await Permission.storage.request();
|
||||||
|
// 申请录音权限
|
||||||
|
var microphoneStatus = await Permission.microphone.request();
|
||||||
|
|
||||||
|
if (storageStatus.isGranted && microphoneStatus.isGranted) {
|
||||||
|
print("Permissions granted");
|
||||||
|
} else {
|
||||||
|
print("Permissions denied");
|
||||||
|
// 如果权限被拒绝,可以提示用户或跳转到设置页面
|
||||||
|
if (await Permission.storage.isPermanentlyDenied) {
|
||||||
|
openAppSettings(); // 跳转到应用设置页面
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startRecording() async {
|
||||||
|
// requestPermissions();
|
||||||
|
// if (state.isRecordingScreen.value) {
|
||||||
|
// showToast('录屏已开始,请勿重复点击');
|
||||||
|
// }
|
||||||
|
// bool start = await FlutterScreenRecording.startRecordScreen(
|
||||||
|
// "Screen Recording", // 视频文件名
|
||||||
|
// titleNotification: "Recording in progress", // 通知栏标题
|
||||||
|
// messageNotification: "Tap to stop recording", // 通知栏内容
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// if (start) {
|
||||||
|
// state.isRecordingScreen.value = true;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopRecording() async {
|
||||||
|
// String path = await FlutterScreenRecording.stopRecordScreen;
|
||||||
|
// print("Recording saved to: $path");
|
||||||
|
//
|
||||||
|
// // 将视频保存到系统相册
|
||||||
|
// bool? success = await GallerySaver.saveVideo(path);
|
||||||
|
// if (success == true) {
|
||||||
|
// print("Video saved to gallery");
|
||||||
|
// } else {
|
||||||
|
// print("Failed to save video to gallery");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// showToast('录屏结束,已保存到系统相册');
|
||||||
|
// state.isRecordingScreen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onReady() {
|
||||||
|
super.onReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
// 启动监听音视频数据流
|
||||||
|
_startListenTalkData();
|
||||||
|
// 启动监听对讲状态
|
||||||
|
_startListenTalkStatus();
|
||||||
|
// 在没有监听成功之前赋值一遍状态
|
||||||
|
// *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 ***
|
||||||
|
state.talkStatus.value = state.startChartTalkStatus.status;
|
||||||
|
|
||||||
|
// 初始化音频播放器
|
||||||
|
_initFlutterPcmSound();
|
||||||
|
|
||||||
|
// 启动播放定时器
|
||||||
|
// _startPlayback();
|
||||||
|
|
||||||
|
// 初始化录音控制器
|
||||||
|
_initAudioRecorder();
|
||||||
|
|
||||||
|
requestPermissions();
|
||||||
|
|
||||||
|
// 启动视频渲染定时器(10fps)
|
||||||
|
videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
final int now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (state.videoBuffer.isNotEmpty) {
|
||||||
|
final TalkData oldestFrame = state.videoBuffer.removeAt(0);
|
||||||
|
if (oldestFrame.content.isNotEmpty) {
|
||||||
|
state.listData.value =
|
||||||
|
Uint8List.fromList(oldestFrame.content); // 备份原始数据
|
||||||
|
final int decodeStart = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
decodeImageFromList(Uint8List.fromList(oldestFrame.content))
|
||||||
|
.then((ui.Image img) {
|
||||||
|
final int decodeEnd = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
state.currentImage.value = img;
|
||||||
|
_renderedFrameCount++;
|
||||||
|
// 每秒统计一次fps
|
||||||
|
if (now - _lastFpsPrintTime >= 1000) {
|
||||||
|
// print('实际渲染fps: $_renderedFrameCount');
|
||||||
|
_renderedFrameCount = 0;
|
||||||
|
_lastFpsPrintTime = now;
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
print('图片解码失败: $e');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果缓冲区为空,不做任何操作,保持上一次内容
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_stopPlayG711Data(); // 停止播放音频
|
||||||
|
state.listData.value = Uint8List(0); // 清空视频数据
|
||||||
|
state.audioBuffer.clear(); // 清空音频缓冲区
|
||||||
|
state.videoBuffer.clear(); // 清空视频缓冲区
|
||||||
|
|
||||||
|
state.oneMinuteTimeTimer?.cancel();
|
||||||
|
state.oneMinuteTimeTimer = null;
|
||||||
|
|
||||||
|
stopProcessingAudio();
|
||||||
|
// 清理图片缓存
|
||||||
|
// _imageCache.clear();
|
||||||
|
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||||
|
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
||||||
|
state.oneMinuteTime.value = 0;
|
||||||
|
// 取消数据流监听
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
_isListening = false;
|
||||||
|
|
||||||
|
// 释放视频渲染定时器
|
||||||
|
videoRenderTimer?.cancel();
|
||||||
|
videoRenderTimer = null;
|
||||||
|
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
stopProcessingAudio();
|
||||||
|
// 重置期望数据
|
||||||
|
StartChartManage().reSetDefaultTalkExpect();
|
||||||
|
// 释放视频渲染定时器
|
||||||
|
videoRenderTimer?.cancel();
|
||||||
|
videoRenderTimer = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理无效通话状态
|
||||||
|
void _handleInvalidTalkStatus() {
|
||||||
|
state.listData.value = Uint8List(0);
|
||||||
|
// 停止播放音频
|
||||||
|
_stopPlayG711Data();
|
||||||
|
stopProcessingAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新发送预期数据
|
||||||
|
void updateTalkExpect() {
|
||||||
|
TalkExpectReq talkExpectReq = TalkExpectReq();
|
||||||
|
state.isOpenVoice.value = !state.isOpenVoice.value;
|
||||||
|
if (!state.isOpenVoice.value) {
|
||||||
|
talkExpectReq = TalkExpectReq(
|
||||||
|
videoType: [VideoTypeE.IMAGE],
|
||||||
|
audioType: [],
|
||||||
|
);
|
||||||
|
showToast('已静音'.tr);
|
||||||
|
} else {
|
||||||
|
talkExpectReq = TalkExpectReq(
|
||||||
|
videoType: [VideoTypeE.IMAGE],
|
||||||
|
audioType: [AudioTypeE.G711],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 修改发送预期数据
|
||||||
|
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||||
|
talkExpect: talkExpectReq);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 截图并保存到相册
|
||||||
|
Future<void> captureAndSavePng() async {
|
||||||
|
try {
|
||||||
|
if (state.globalKey.currentContext == null) {
|
||||||
|
AppLog.log('截图失败: 未找到当前上下文');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final RenderRepaintBoundary boundary = state.globalKey.currentContext!
|
||||||
|
.findRenderObject()! as RenderRepaintBoundary;
|
||||||
|
final ui.Image image = await boundary.toImage();
|
||||||
|
final ByteData? byteData =
|
||||||
|
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
|
||||||
|
if (byteData == null) {
|
||||||
|
AppLog.log('截图失败: 图像数据为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Uint8List pngBytes = byteData.buffer.asUint8List();
|
||||||
|
|
||||||
|
// 获取应用程序的文档目录
|
||||||
|
final Directory directory = await getApplicationDocumentsDirectory();
|
||||||
|
final String imagePath = '${directory.path}/screenshot.png';
|
||||||
|
|
||||||
|
// 将截图保存为文件
|
||||||
|
final File imgFile = File(imagePath);
|
||||||
|
await imgFile.writeAsBytes(pngBytes);
|
||||||
|
|
||||||
|
// 将截图保存到相册
|
||||||
|
await ImageGallerySaver.saveFile(imagePath);
|
||||||
|
|
||||||
|
AppLog.log('截图保存路径: $imagePath');
|
||||||
|
showToast('截图已保存到相册'.tr);
|
||||||
|
} catch (e) {
|
||||||
|
AppLog.log('截图失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程开锁
|
||||||
|
Future<void> remoteOpenLock() async {
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
|
||||||
|
var lockId = currentKeyInfo.lockId ?? 0;
|
||||||
|
var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0;
|
||||||
|
|
||||||
|
final lockPeerId = StartChartManage().lockPeerId;
|
||||||
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
|
final lockList = element.lockList;
|
||||||
|
if (lockList != null && lockList.length != 0) {
|
||||||
|
for (var lockInfo in lockList) {
|
||||||
|
final peerId = lockInfo.network?.peerId;
|
||||||
|
if (peerId != null && peerId != '') {
|
||||||
|
if (peerId == lockPeerId) {
|
||||||
|
lockId = lockInfo.lockId ?? 0;
|
||||||
|
remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (remoteUnlock == 1) {
|
||||||
|
final LoginEntity entity = await ApiRepository.to
|
||||||
|
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
||||||
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
|
showToast('已开锁'.tr);
|
||||||
|
StartChartManage().lockListPeerId = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast('该锁的远程开锁功能未启用'.tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化音频录制器
|
||||||
|
void _initAudioRecorder() {
|
||||||
|
state.voiceProcessor = VoiceProcessor.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
//开始录音
|
||||||
|
Future<void> startProcessingAudio() async {
|
||||||
|
try {
|
||||||
|
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||||
|
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||||
|
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||||
|
state.isRecordingAudio.value = isRecording!;
|
||||||
|
state.startRecordingAudioTime.value = DateTime.now();
|
||||||
|
|
||||||
|
// 增加录音帧监听器和错误监听器
|
||||||
|
state.voiceProcessor
|
||||||
|
?.addFrameListeners(<VoiceProcessorFrameListener>[_onFrame]);
|
||||||
|
state.voiceProcessor?.addErrorListener(_onError);
|
||||||
|
} else {
|
||||||
|
// state.errorMessage.value = 'Recording permission not granted';
|
||||||
|
}
|
||||||
|
} on PlatformException catch (ex) {
|
||||||
|
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||||
|
}
|
||||||
|
state.isOpenVoice.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 停止录音
|
||||||
|
Future<void> stopProcessingAudio() async {
|
||||||
|
try {
|
||||||
|
await state.voiceProcessor?.stop();
|
||||||
|
state.voiceProcessor?.removeFrameListener(_onFrame);
|
||||||
|
state.udpSendDataFrameNumber = 0;
|
||||||
|
// 记录结束时间
|
||||||
|
state.endRecordingAudioTime.value = DateTime.now();
|
||||||
|
|
||||||
|
// 计算录音的持续时间
|
||||||
|
final Duration duration = state.endRecordingAudioTime.value
|
||||||
|
.difference(state.startRecordingAudioTime.value);
|
||||||
|
|
||||||
|
state.recordingAudioTime.value = duration.inSeconds;
|
||||||
|
} on PlatformException catch (ex) {
|
||||||
|
// state.errorMessage.value = 'Failed to stop recorder: $ex';
|
||||||
|
} finally {
|
||||||
|
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||||
|
state.isRecordingAudio.value = isRecording!;
|
||||||
|
state.isOpenVoice.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频帧处理
|
||||||
|
Future<void> _onFrame(List<int> frame) async {
|
||||||
|
// 添加最大缓冲限制
|
||||||
|
if (_bufferedAudioFrames.length > state.frameLength * 3) {
|
||||||
|
_bufferedAudioFrames.clear(); // 清空过多积累的数据
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先应用固定增益提升基础音量
|
||||||
|
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||||
|
// 编码为G711数据
|
||||||
|
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||||
|
_bufferedAudioFrames.addAll(encodedData);
|
||||||
|
// 使用相对时间戳
|
||||||
|
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||||
|
int getFrameLength = state.frameLength;
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
getFrameLength = state.frameLength * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加发送间隔控制
|
||||||
|
if (_bufferedAudioFrames.length >= state.frameLength) {
|
||||||
|
try {
|
||||||
|
await StartChartManage().sendTalkDataMessage(
|
||||||
|
talkData: TalkData(
|
||||||
|
content: _bufferedAudioFrames,
|
||||||
|
contentType: TalkData_ContentTypeE.G711,
|
||||||
|
durationMs: ms,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
_bufferedAudioFrames.clear(); // 确保清理缓冲区
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_bufferedAudioFrames.addAll(encodedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误监听
|
||||||
|
void _onError(VoiceProcessorException error) {
|
||||||
|
AppLog.log(error.message!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加音频增益处理方法
|
||||||
|
List<int> _applyGain(List<int> pcmData, double gainFactor) {
|
||||||
|
List<int> result = List<int>.filled(pcmData.length, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < pcmData.length; i++) {
|
||||||
|
// PCM数据通常是有符号的16位整数
|
||||||
|
int sample = pcmData[i];
|
||||||
|
|
||||||
|
// 应用增益
|
||||||
|
double amplified = sample * gainFactor;
|
||||||
|
|
||||||
|
// 限制在有效范围内,防止溢出
|
||||||
|
if (amplified > 32767) {
|
||||||
|
amplified = 32767;
|
||||||
|
} else if (amplified < -32768) {
|
||||||
|
amplified = -32768;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = amplified.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,238 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_colors.dart';
|
||||||
|
import 'package:star_lock/talk/call/callTalk.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_logic.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_state.dart';
|
||||||
|
import 'package:star_lock/tools/titleAppBar.dart';
|
||||||
|
import 'package:slide_to_act/slide_to_act.dart';
|
||||||
|
|
||||||
|
// 可选:引入第三方滑动解锁库
|
||||||
|
// import 'package:flutter_slider_button/flutter_slider_button.dart';
|
||||||
|
|
||||||
|
class ImageTransmissionPage extends StatefulWidget {
|
||||||
|
const ImageTransmissionPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageTransmissionPage> createState() => _ImageTransmissionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageTransmissionPageState extends State<ImageTransmissionPage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
final ImageTransmissionLogic logic = Get.put(ImageTransmissionLogic());
|
||||||
|
final ImageTransmissionState state = Get.find<ImageTransmissionLogic>().state;
|
||||||
|
final startChartManage = StartChartManage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
state.animationController = AnimationController(
|
||||||
|
vsync: this, // 确保使用的TickerProvider是当前Widget
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
);
|
||||||
|
state.animationController.repeat();
|
||||||
|
state.animationController.addStatusListener((AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
state.animationController.reset();
|
||||||
|
state.animationController.forward();
|
||||||
|
} else if (status == AnimationStatus.dismissed) {
|
||||||
|
state.animationController.reset();
|
||||||
|
state.animationController.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
state.animationController.dispose();
|
||||||
|
CallTalk().finishAVData();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: AppColors.mainBackgroundColor,
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
appBar: TitleAppBar(
|
||||||
|
barTitle: '图传'.tr,
|
||||||
|
haveBack: true,
|
||||||
|
backgroundColor: AppColors.mainColor,
|
||||||
|
backAction: () {
|
||||||
|
logic.udpHangUpAction();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
body: Obx(() => Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24.h),
|
||||||
|
SizedBox(
|
||||||
|
height: 0.6.sh,
|
||||||
|
child: state.listData.value.isEmpty
|
||||||
|
? _buildWaitingView()
|
||||||
|
: _buildVideoView(),
|
||||||
|
),
|
||||||
|
SizedBox(height: 30.h),
|
||||||
|
_buildBottomToolBar(),
|
||||||
|
SizedBox(height: 30.h),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWaitingView() {
|
||||||
|
double barWidth = MediaQuery.of(context).size.width - 60.w;
|
||||||
|
return Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(30.h),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: barWidth,
|
||||||
|
height: double.infinity,
|
||||||
|
child: Image.asset(
|
||||||
|
'images/main/monitorBg.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RotationTransition(
|
||||||
|
turns: state.animationController,
|
||||||
|
child: Image.asset(
|
||||||
|
'images/main/realTime_connecting.png',
|
||||||
|
width: 300.w,
|
||||||
|
height: 300.w,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoView() {
|
||||||
|
double barWidth = MediaQuery.of(context).size.width - 60.w;
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
key: state.globalKey,
|
||||||
|
child: Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(30.h),
|
||||||
|
child: Container(
|
||||||
|
width: barWidth,
|
||||||
|
height: double.infinity,
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||||
|
child: RawImage(
|
||||||
|
image: state.currentImage.value,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomToolBar() {
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 30.w),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 20.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(30.h),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_circleButton(
|
||||||
|
icon: Icons.call,
|
||||||
|
color: Colors.green,
|
||||||
|
onTap: () {
|
||||||
|
if (state.talkStatus.value ==
|
||||||
|
TalkStatus.passiveCallWaitingAnswer) {
|
||||||
|
// 接听
|
||||||
|
logic.initiateAnswerCommand();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_circleButton(
|
||||||
|
icon: Icons.call_end,
|
||||||
|
color: Colors.red,
|
||||||
|
onTap: () {
|
||||||
|
logic.udpHangUpAction();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_circleButton(
|
||||||
|
icon: Icons.camera_alt,
|
||||||
|
color: Colors.blue,
|
||||||
|
onTap: () async {
|
||||||
|
await logic.captureAndSavePng();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 36.h),
|
||||||
|
SlideAction(
|
||||||
|
height: 64.h,
|
||||||
|
borderRadius: 24.h,
|
||||||
|
elevation: 0,
|
||||||
|
innerColor: Colors.amber,
|
||||||
|
outerColor: Colors.amber.withOpacity(0.15),
|
||||||
|
sliderButtonIcon: Icon(Icons.lock, color: Colors.white, size: 40.w),
|
||||||
|
text: '滑动解锁',
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontSize: 26.sp,
|
||||||
|
color: Colors.black54,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
|
onSubmit: () {
|
||||||
|
// TODO: 实现滑动解锁逻辑
|
||||||
|
logic.remoteOpenLock();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _circleButton(
|
||||||
|
{required IconData icon,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback onTap}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 90.w,
|
||||||
|
height: 90.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withOpacity(0.3),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 48.w),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get/get_rx/get_rx.dart';
|
||||||
|
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||||
|
import 'package:get/state_manager.dart';
|
||||||
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
||||||
|
|
||||||
|
import '../../../../tools/storage.dart';
|
||||||
|
|
||||||
|
enum NetworkStatus {
|
||||||
|
normal, // 0
|
||||||
|
lagging, // 1
|
||||||
|
delayed, // 2
|
||||||
|
packetLoss // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageTransmissionState{
|
||||||
|
int udpSendDataFrameNumber = 0; // 帧序号
|
||||||
|
// var isSenderAudioData = false.obs;// 是否要发送音频数据
|
||||||
|
|
||||||
|
Future<String?> userMobileIP = NetworkInfo().getWifiIP();
|
||||||
|
Future<String?> userUid = Storage.getUid();
|
||||||
|
|
||||||
|
RxInt udpStatus =
|
||||||
|
0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话
|
||||||
|
TextEditingController passwordTF = TextEditingController();
|
||||||
|
|
||||||
|
Rx<Uint8List> listData = Uint8List(0).obs; //得到的视频流字节数据
|
||||||
|
RxList<int> listAudioData = <int>[].obs; //得到的音频流字节数据
|
||||||
|
GlobalKey globalKey = GlobalKey();
|
||||||
|
|
||||||
|
Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面
|
||||||
|
RxInt oneMinuteTime = 0.obs; // 定时器秒数
|
||||||
|
|
||||||
|
// 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次
|
||||||
|
late Timer answerTimer;
|
||||||
|
late Timer hangUpTimer;
|
||||||
|
late Timer openDoorTimer;
|
||||||
|
Timer? fpsTimer;
|
||||||
|
late AnimationController animationController;
|
||||||
|
|
||||||
|
late Timer autoBackTimer =
|
||||||
|
Timer(const Duration(seconds: 1), () {}); //发送30秒监视后自动返回
|
||||||
|
late Timer realTimePicTimer =
|
||||||
|
Timer(const Duration(seconds: 1), () {}); //监视命令定时器
|
||||||
|
RxInt elapsedSeconds = 0.obs;
|
||||||
|
|
||||||
|
// 星图对讲相关状态
|
||||||
|
List<TalkData> audioBuffer = <TalkData>[].obs;
|
||||||
|
List<TalkData> activeAudioBuffer = <TalkData>[].obs;
|
||||||
|
List<TalkData> activeVideoBuffer = <TalkData>[].obs;
|
||||||
|
|
||||||
|
List<TalkData> videoBuffer = <TalkData>[].obs;
|
||||||
|
List<TalkData> videoBuffer2 = <TalkData>[].obs;
|
||||||
|
RxBool isPlaying = false.obs; // 是否开始播放
|
||||||
|
Rx<TalkStatus> talkStatus = TalkStatus.none.obs; //星图对讲状态
|
||||||
|
// 获取 startChartTalkStatus 的唯一实例
|
||||||
|
final StartChartTalkStatus startChartTalkStatus =
|
||||||
|
StartChartTalkStatus.instance;
|
||||||
|
|
||||||
|
// 通话数据流的单例流数据处理类
|
||||||
|
final TalkDataRepository talkDataRepository = TalkDataRepository.instance;
|
||||||
|
RxInt lastFrameTimestamp = 0.obs; // 上一帧的时间戳,用来判断网络环境
|
||||||
|
Rx<NetworkStatus> networkStatus =
|
||||||
|
NetworkStatus.normal.obs; // 网络状态:0-正常 1-网络卡顿 2-网络延迟 3-网络丢包
|
||||||
|
RxInt alertCount = 0.obs; // 网络状态提示计数器
|
||||||
|
RxInt maxAlertNumber = 3.obs; // 网络状态提示最大提示次数
|
||||||
|
RxBool isOpenVoice = true.obs; // 是否打开声音
|
||||||
|
RxBool isRecordingScreen = false.obs; // 是否录屏中
|
||||||
|
RxBool isRecordingAudio = false.obs; // 是否录音中
|
||||||
|
Rx<DateTime> startRecordingAudioTime = DateTime.now().obs; // 开始录音时间
|
||||||
|
Rx<DateTime> endRecordingAudioTime = DateTime.now().obs; // 结束录音时间
|
||||||
|
RxInt recordingAudioTime = 0.obs; // 录音时间持续时间
|
||||||
|
RxInt fps = 0.obs; // 添加 FPS 计数
|
||||||
|
late VoiceProcessor? voiceProcessor; // 音频处理器、录音
|
||||||
|
final int frameLength = 320; //录音视频帧长度为640
|
||||||
|
final int sampleRate = 8000; //录音频采样率为8000
|
||||||
|
List<int> recordingAudioAllFrames = <int>[]; // 录制音频的所有帧
|
||||||
|
List<int> lockRecordingAudioAllFrames = <int>[]; // 录制音频的所有帧
|
||||||
|
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
||||||
|
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||||
|
RxBool hasAudioData = false.obs; // 是否有音频数据
|
||||||
|
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
||||||
|
Rx<ui.Image?> currentImage = Rx<ui.Image?>(null);
|
||||||
|
}
|
||||||
1322
lib/talk/starChart/views/native/talk_view_native_decode_logic.dart
Normal file
1322
lib/talk/starChart/views/native/talk_view_native_decode_logic.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,534 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
|
import 'package:star_lock/talk/call/callTalk.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
|
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||||
|
|
||||||
|
import '../../../../app_settings/app_colors.dart';
|
||||||
|
import '../../../../tools/showTFView.dart';
|
||||||
|
|
||||||
|
class TalkViewNativeDecodePage extends StatefulWidget {
|
||||||
|
const TalkViewNativeDecodePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TalkViewNativeDecodePage> createState() =>
|
||||||
|
_TalkViewNativeDecodePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic());
|
||||||
|
final TalkViewNativeDecodeState state =
|
||||||
|
Get.find<TalkViewNativeDecodeLogic>().state;
|
||||||
|
final startChartManage = StartChartManage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
state.animationController = AnimationController(
|
||||||
|
vsync: this, // 确保使用的TickerProvider是当前Widget
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.animationController.repeat();
|
||||||
|
//动画开始、结束、向前移动或向后移动时会调用StatusListener
|
||||||
|
state.animationController.addStatusListener((AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
state.animationController.reset();
|
||||||
|
state.animationController.forward();
|
||||||
|
} else if (status == AnimationStatus.dismissed) {
|
||||||
|
state.animationController.reset();
|
||||||
|
state.animationController.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
// 返回 false 表示禁止退出
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: SizedBox(
|
||||||
|
width: 1.sw,
|
||||||
|
height: 1.sh,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
// 悬浮帧率统计信息条
|
||||||
|
Obx(
|
||||||
|
() {
|
||||||
|
final double screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final double screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
|
final double logicalWidth = MediaQuery.of(context).size.width;
|
||||||
|
final double logicalHeight = MediaQuery.of(context).size.height;
|
||||||
|
final double devicePixelRatio =
|
||||||
|
MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
// 计算物理像素值
|
||||||
|
final double physicalWidth = logicalWidth * devicePixelRatio;
|
||||||
|
final double physicalHeight = logicalHeight * devicePixelRatio;
|
||||||
|
|
||||||
|
// 旋转后的图片尺寸
|
||||||
|
const int rotatedImageWidth = 480; // 原始高度
|
||||||
|
const int rotatedImageHeight = 864; // 原始宽度
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
final double scaleWidth = physicalWidth / rotatedImageWidth;
|
||||||
|
final double scaleHeight = physicalHeight / rotatedImageHeight;
|
||||||
|
max(scaleWidth, scaleHeight); // 选择较大的缩放比例
|
||||||
|
// 防御性处理:只要loading中或textureId为null,优先渲染loading/占位
|
||||||
|
if (state.isLoading.isTrue || state.textureId.value == null) {
|
||||||
|
return Image.asset(
|
||||||
|
'images/main/monitorBg.png',
|
||||||
|
width: screenWidth,
|
||||||
|
height: screenHeight,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Positioned.fill(
|
||||||
|
child: PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
key: state.globalKey,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: RotatedBox(
|
||||||
|
// 解码器不支持硬件旋转,使用RotatedBox
|
||||||
|
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||||
|
child: Platform.isIOS
|
||||||
|
? Transform.scale(
|
||||||
|
scale: 1.008, // 轻微放大,消除iOS白边
|
||||||
|
child: Texture(
|
||||||
|
textureId: state.textureId.value!,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Texture(
|
||||||
|
textureId: state.textureId.value!,
|
||||||
|
filterQuality: FilterQuality.medium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
Obx(() => state.isLoading.isTrue
|
||||||
|
? Positioned(
|
||||||
|
bottom: 310.h,
|
||||||
|
child: Text(
|
||||||
|
'正在创建安全连接...'.tr,
|
||||||
|
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
||||||
|
))
|
||||||
|
: Container()),
|
||||||
|
Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0
|
||||||
|
? Positioned(
|
||||||
|
top: ScreenUtil().statusBarHeight + 75.h,
|
||||||
|
width: 1.sw,
|
||||||
|
child: Obx(
|
||||||
|
() {
|
||||||
|
final String sec = (state.oneMinuteTime.value % 60)
|
||||||
|
.toString()
|
||||||
|
.padLeft(2, '0');
|
||||||
|
final String min = (state.oneMinuteTime.value ~/ 60)
|
||||||
|
.toString()
|
||||||
|
.padLeft(2, '0');
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$min:$sec',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26.sp, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container()),
|
||||||
|
Positioned(
|
||||||
|
bottom: 10.w,
|
||||||
|
child: Container(
|
||||||
|
width: 1.sw - 30.w * 2,
|
||||||
|
// height: 300.h,
|
||||||
|
margin: EdgeInsets.all(30.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20.h)),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
bottomTopBtnWidget(),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
bottomBottomBtnWidget(),
|
||||||
|
SizedBox(height: 20.h),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(() => state.isLoading.isTrue
|
||||||
|
? buildRotationTransition()
|
||||||
|
: Container()),
|
||||||
|
Obx(() => state.isLongPressing.value
|
||||||
|
? Positioned(
|
||||||
|
top: 80.h,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(10.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(10.w),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(Icons.mic, color: Colors.white, size: 24.w),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Text(
|
||||||
|
'正在说话...'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20.sp, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bottomTopBtnWidget() {
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||||
|
// 打开关闭声音
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||||
|
// 打开关闭声音
|
||||||
|
logic.updateTalkExpect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
padding: EdgeInsets.all(5.w),
|
||||||
|
child: Obx(() => Image(
|
||||||
|
width: 40.w,
|
||||||
|
height: 40.w,
|
||||||
|
image: state.isOpenVoice.value
|
||||||
|
? const AssetImage(
|
||||||
|
'images/main/icon_lockDetail_monitoringOpenVoice.png')
|
||||||
|
: const AssetImage(
|
||||||
|
'images/main/icon_lockDetail_monitoringCloseVoice.png'))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 50.w),
|
||||||
|
// 截图
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||||
|
await logic.captureAndSavePng();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
padding: EdgeInsets.all(5.w),
|
||||||
|
child: Image(
|
||||||
|
width: 40.w,
|
||||||
|
height: 40.w,
|
||||||
|
image: const AssetImage(
|
||||||
|
'images/main/icon_lockDetail_monitoringScreenshot.png')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 50.w),
|
||||||
|
// 录制
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
logic.showToast('功能暂未开放'.tr);
|
||||||
|
// if (
|
||||||
|
// state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||||
|
// if (state.isRecordingScreen.value) {
|
||||||
|
// await logic.stopRecording();
|
||||||
|
// } else {
|
||||||
|
// await logic.startRecording();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 50.w,
|
||||||
|
height: 50.w,
|
||||||
|
padding: EdgeInsets.all(5.w),
|
||||||
|
child: Image(
|
||||||
|
width: 40.w,
|
||||||
|
height: 40.w,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
image: const AssetImage(
|
||||||
|
'images/main/icon_lockDetail_monitoringScreenRecording.png'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 50.w),
|
||||||
|
// 清晰度切换按钮
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
// 弹出底部弹出层,选择清晰度
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)),
|
||||||
|
),
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
final List<String> qualities = ['高清', '标清'];
|
||||||
|
return SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: qualities.map((q) {
|
||||||
|
return Obx(() => InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
logic.onQualityChanged(q);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 18.w),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
q,
|
||||||
|
style: TextStyle(
|
||||||
|
color: state.currentQuality.value == q
|
||||||
|
? AppColors.mainColor
|
||||||
|
: Colors.black,
|
||||||
|
fontWeight: state.currentQuality.value == q
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
fontSize: 28.sp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
child: Icon(Icons.high_quality_outlined, color: Colors.white, size: 38.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bottomBottomBtnWidget() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
// 接听
|
||||||
|
Obx(
|
||||||
|
() => bottomBtnItemWidget(
|
||||||
|
getAnswerBtnImg(),
|
||||||
|
getAnswerBtnName(),
|
||||||
|
Colors.white,
|
||||||
|
longPress: () async {
|
||||||
|
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||||
|
// 启动录音
|
||||||
|
logic.startProcessingAudio();
|
||||||
|
state.isLongPressing.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
longPressUp: () async {
|
||||||
|
// 停止录音
|
||||||
|
logic.stopProcessingAudio();
|
||||||
|
state.isLongPressing.value = false;
|
||||||
|
},
|
||||||
|
onClick: () async {
|
||||||
|
if (state.talkStatus.value ==
|
||||||
|
TalkStatus.passiveCallWaitingAnswer) {
|
||||||
|
// 接听
|
||||||
|
logic.initiateAnswerCommand();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomBtnItemWidget(
|
||||||
|
'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red,
|
||||||
|
onClick: () {
|
||||||
|
// 挂断
|
||||||
|
logic.udpHangUpAction();
|
||||||
|
}),
|
||||||
|
bottomBtnItemWidget(
|
||||||
|
'images/main/icon_lockDetail_monitoringUnlock.png',
|
||||||
|
'开锁'.tr,
|
||||||
|
AppColors.mainColor,
|
||||||
|
onClick: () {
|
||||||
|
// if (state.talkStatus.value == TalkStatus.answeredSuccessfully &&
|
||||||
|
// state.listData.value.length > 0) {
|
||||||
|
// logic.udpOpenDoorAction();
|
||||||
|
// }
|
||||||
|
// if (UDPManage().remoteUnlock == 1) {
|
||||||
|
// logic.udpOpenDoorAction();
|
||||||
|
// showDeletPasswordAlertDialog(context);
|
||||||
|
// } else {
|
||||||
|
// logic.showToast('请在锁设置中开启远程开锁'.tr);
|
||||||
|
// }
|
||||||
|
logic.remoteOpenLock();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAnswerBtnImg() {
|
||||||
|
switch (state.talkStatus.value) {
|
||||||
|
case TalkStatus.passiveCallWaitingAnswer:
|
||||||
|
return 'images/main/icon_lockDetail_monitoringAnswerCalls.png';
|
||||||
|
case TalkStatus.answeredSuccessfully:
|
||||||
|
case TalkStatus.proactivelyCallWaitingAnswer:
|
||||||
|
return 'images/main/icon_lockDetail_monitoringUnTalkback.png';
|
||||||
|
default:
|
||||||
|
return 'images/main/icon_lockDetail_monitoringAnswerCalls.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAnswerBtnName() {
|
||||||
|
switch (state.talkStatus.value) {
|
||||||
|
case TalkStatus.passiveCallWaitingAnswer:
|
||||||
|
return '接听'.tr;
|
||||||
|
case TalkStatus.proactivelyCallWaitingAnswer:
|
||||||
|
case TalkStatus.answeredSuccessfully:
|
||||||
|
return '长按说话'.tr;
|
||||||
|
default:
|
||||||
|
return '接听'.tr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bottomBtnItemWidget(
|
||||||
|
String iconUrl,
|
||||||
|
String name,
|
||||||
|
Color backgroundColor, {
|
||||||
|
required Function() onClick,
|
||||||
|
Function()? longPress,
|
||||||
|
Function()? longPressUp,
|
||||||
|
}) {
|
||||||
|
double wh = 80.w;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onClick,
|
||||||
|
onLongPress: longPress,
|
||||||
|
onLongPressUp: longPressUp,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 160.w,
|
||||||
|
width: 140.w,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: wh,
|
||||||
|
height: wh,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: wh,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(20.w),
|
||||||
|
child: Image.asset(iconUrl, fit: BoxFit.fitWidth),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20.w),
|
||||||
|
Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(fontSize: 20.sp, color: Colors.white),
|
||||||
|
textAlign: TextAlign.center, // 当文本超出指定行数时,使用省略号表示
|
||||||
|
maxLines: 2, // 设置最大行数为1
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据丢包率返回对应的颜色
|
||||||
|
Color _getPacketLossColor(double lossRate) {
|
||||||
|
if (lossRate < 1.0) {
|
||||||
|
return Colors.green; // 丢包率低于1%显示绿色
|
||||||
|
} else if (lossRate < 5.0) {
|
||||||
|
return Colors.yellow; // 丢包率1%-5%显示黄色
|
||||||
|
} else if (lossRate < 10.0) {
|
||||||
|
return Colors.orange; // 丢包率5%-10%显示橙色
|
||||||
|
} else {
|
||||||
|
return Colors.red; // 丢包率高于10%显示红色
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//旋转动画
|
||||||
|
Widget buildRotationTransition() {
|
||||||
|
return Positioned(
|
||||||
|
left: ScreenUtil().screenWidth / 2 - 220.w / 2,
|
||||||
|
top: ScreenUtil().screenHeight / 2 - 220.w / 2 - 150.h,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: RotationTransition(
|
||||||
|
//设置动画的旋转中心
|
||||||
|
alignment: Alignment.center,
|
||||||
|
//动画控制器
|
||||||
|
turns: state.animationController,
|
||||||
|
//将要执行动画的子view
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
child: Image.asset(
|
||||||
|
'images/main/realTime_connecting.png',
|
||||||
|
width: 220.w,
|
||||||
|
height: 220.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
state.animationController.forward();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
state.animationController.dispose();
|
||||||
|
CallTalk().finishAVData();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get/get_rx/get_rx.dart';
|
||||||
|
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||||
|
import 'package:get/state_manager.dart';
|
||||||
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
||||||
|
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||||
|
|
||||||
|
import '../../../../tools/storage.dart';
|
||||||
|
|
||||||
|
enum NetworkStatus {
|
||||||
|
normal, // 0
|
||||||
|
lagging, // 1
|
||||||
|
delayed, // 2
|
||||||
|
packetLoss // 3
|
||||||
|
}
|
||||||
|
|
||||||
|
class TalkViewNativeDecodeState {
|
||||||
|
// 视频源最大帧率限制
|
||||||
|
static const int maxSourceFps = 25; // 视频源最高支持25fps
|
||||||
|
|
||||||
|
int udpSendDataFrameNumber = 0; // 帧序号
|
||||||
|
// var isSenderAudioData = false.obs;// 是否要发送音频数据
|
||||||
|
|
||||||
|
Future<String?> userMobileIP = NetworkInfo().getWifiIP();
|
||||||
|
Future<String?> userUid = Storage.getUid();
|
||||||
|
|
||||||
|
RxInt udpStatus =
|
||||||
|
0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话
|
||||||
|
TextEditingController passwordTF = TextEditingController();
|
||||||
|
|
||||||
|
RxList<int> listAudioData = <int>[].obs; //得到的音频流字节数据
|
||||||
|
GlobalKey globalKey = GlobalKey();
|
||||||
|
|
||||||
|
Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面
|
||||||
|
RxInt oneMinuteTime = 0.obs; // 定时器秒数
|
||||||
|
|
||||||
|
// 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次
|
||||||
|
late Timer answerTimer;
|
||||||
|
late Timer hangUpTimer;
|
||||||
|
late Timer openDoorTimer;
|
||||||
|
Timer? fpsTimer;
|
||||||
|
late AnimationController animationController;
|
||||||
|
|
||||||
|
RxInt elapsedSeconds = 0.obs;
|
||||||
|
|
||||||
|
// 星图对讲相关状态
|
||||||
|
List<TalkData> audioBuffer = <TalkData>[].obs;
|
||||||
|
|
||||||
|
RxBool isLoading = true.obs; // 是否在加载
|
||||||
|
RxBool isPlaying = false.obs; // 是否开始播放
|
||||||
|
Rx<TalkStatus> talkStatus = TalkStatus.none.obs; //星图对讲状态
|
||||||
|
// 获取 startChartTalkStatus 的唯一实例
|
||||||
|
final StartChartTalkStatus startChartTalkStatus =
|
||||||
|
StartChartTalkStatus.instance;
|
||||||
|
|
||||||
|
// 通话数据流的单例流数据处理类
|
||||||
|
final TalkDataRepository talkDataRepository = TalkDataRepository.instance;
|
||||||
|
|
||||||
|
RxBool isOpenVoice = true.obs; // 是否打开声音
|
||||||
|
RxBool isRecordingScreen = false.obs; // 是否录屏中
|
||||||
|
RxBool isRecordingAudio = false.obs; // 是否录音中
|
||||||
|
Rx<DateTime> startRecordingAudioTime = DateTime.now().obs; // 开始录音时间
|
||||||
|
Rx<DateTime> endRecordingAudioTime = DateTime.now().obs; // 结束录音时间
|
||||||
|
RxInt recordingAudioTime = 0.obs; // 录音时间持续时间
|
||||||
|
late VoiceProcessor? voiceProcessor; // 音频处理器、录音
|
||||||
|
final int frameLength = 320; //录音视频帧长度为640
|
||||||
|
final int sampleRate = 8000; //录音频采样率为8000
|
||||||
|
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||||
|
// 视频解码器纹理ID
|
||||||
|
Rx<int?> textureId = Rx<int?>(null);
|
||||||
|
// FPS监测相关变量
|
||||||
|
|
||||||
|
RxInt lastFpsUpdateTime = 0.obs; // 上次FPS更新时间
|
||||||
|
RxBool showFps = true.obs; // 是否显示FPS
|
||||||
|
// 丢包率统计相关变量
|
||||||
|
RxDouble decoderFps = 0.0.obs; // 消息丢失率
|
||||||
|
RxDouble messageLossRate = 0.0.obs; // 消息丢失率
|
||||||
|
RxDouble packetLossRate = 0.0.obs; // 分包丢失率
|
||||||
|
RxInt lastPacketStatsUpdateTime = 0.obs; // 上次更新丢包统计的时间
|
||||||
|
|
||||||
|
// 解码器详细统计信息
|
||||||
|
RxInt renderedFrameCount = 0.obs; // 已渲染帧数
|
||||||
|
RxInt totalFrames = 0.obs; // 总帧数
|
||||||
|
RxInt droppedFrames = 0.obs; // 丢弃帧数
|
||||||
|
RxBool hasSentIDR = false.obs; // 是否已发送IDR帧
|
||||||
|
RxBool hasSentSPS = false.obs; // 是否已发送SPS
|
||||||
|
RxBool hasSentPPS = false.obs; // 是否已发送PPS
|
||||||
|
RxInt keyFrameInterval = 0.obs; // 关键帧间隔时间(ms)
|
||||||
|
RxInt decodingJitterMs = 0.obs; // 解码抖动时间(ms)
|
||||||
|
|
||||||
|
// 性能分析变量
|
||||||
|
int lastPerformanceCheck = 0;
|
||||||
|
int lastFrameCount = 0;
|
||||||
|
|
||||||
|
// 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq
|
||||||
|
Map<String, Map<String, dynamic>> frameTracker = {};
|
||||||
|
|
||||||
|
// H264帧缓冲区相关
|
||||||
|
final List<Map<String, dynamic>> h264FrameBuffer = <Map<String, dynamic>>[]; // H264帧缓冲区,存储帧数据和类型
|
||||||
|
final int maxFrameBufferSize = 15; // 最大缓冲区大小
|
||||||
|
final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区
|
||||||
|
Timer? frameProcessTimer; // 帧处理定时器
|
||||||
|
bool isProcessingFrame = false; // 是否正在处理帧
|
||||||
|
int lastProcessedTimestamp = 0; // 上次处理帧的时间戳
|
||||||
|
// H264文件保存相关
|
||||||
|
String? h264FilePath;
|
||||||
|
File? h264File;
|
||||||
|
|
||||||
|
// 当前清晰度选项,初始为'高清'
|
||||||
|
RxString currentQuality = '高清'.obs; // 可选:高清、标清、流畅
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
import 'dart:math'; // Import the math package to use sqrt
|
import 'dart:math'; // Import the math package to use sqrt
|
||||||
|
import 'dart:ui' show decodeImageFromList;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@ -23,12 +24,15 @@ import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
|||||||
import 'package:star_lock/network/api_repository.dart';
|
import 'package:star_lock/network/api_repository.dart';
|
||||||
import 'package:star_lock/talk/call/g711.dart';
|
import 'package:star_lock/talk/call/g711.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
import 'package:star_lock/tools/G711Tool.dart';
|
import 'package:star_lock/tools/G711Tool.dart';
|
||||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
|
import 'package:star_lock/tools/commonDataManage.dart';
|
||||||
|
import 'package:star_lock/tools/storage.dart';
|
||||||
|
|
||||||
import '../../../../tools/baseGetXController.dart';
|
import '../../../../tools/baseGetXController.dart';
|
||||||
|
|
||||||
@ -37,32 +41,24 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
|
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
|
||||||
|
|
||||||
final int minBufferSize = 2; // 最小缓冲2帧,约166ms
|
int bufferSize = 8; // 增大缓冲区,满时才渲染
|
||||||
final int maxBufferSize = 20; // 最大缓冲8帧,约666ms
|
|
||||||
int bufferSize = 8; // 初始化为默认大小
|
|
||||||
// 修改音频相关的成员变量
|
|
||||||
final int minAudioBufferSize = 1; // 音频最小缓冲1帧
|
|
||||||
final int maxAudioBufferSize = 3; // 音频最大缓冲3帧
|
|
||||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
|
||||||
|
|
||||||
// 添加开始时间记录
|
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||||
int _startTime = 0; // 开始播放时间戳
|
|
||||||
int _startAudioTime = 0; // 开始播放时间戳
|
|
||||||
bool _isFirstFrame = true; // 是否是第一帧
|
|
||||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||||
|
|
||||||
|
int _startAudioTime = 0; // 开始播放时间戳
|
||||||
|
|
||||||
// 定义音频帧缓冲和发送函数
|
// 定义音频帧缓冲和发送函数
|
||||||
final List<int> _bufferedAudioFrames = <int>[];
|
final List<int> _bufferedAudioFrames = <int>[];
|
||||||
|
|
||||||
final Map<String, ui.Image> _imageCache = {};
|
// 添加监听状态和订阅引用
|
||||||
|
bool _isListening = false;
|
||||||
|
StreamSubscription? _streamSubscription;
|
||||||
|
|
||||||
// 添加一个变量用于记录上一帧的时间戳
|
Timer? videoRenderTimer; // 视频渲染定时器
|
||||||
int _lastFrameTimestamp = 0; // 初始值为 0
|
|
||||||
|
|
||||||
// 添加帧率计算相关变量
|
int _renderedFrameCount = 0;
|
||||||
int _frameCount = 0;
|
int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
int _lastFpsUpdateTime = 0;
|
|
||||||
Timer? _fpsTimer;
|
|
||||||
|
|
||||||
/// 初始化音频播放器
|
/// 初始化音频播放器
|
||||||
void _initFlutterPcmSound() {
|
void _initFlutterPcmSound() {
|
||||||
@ -96,18 +92,28 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
// 监听音视频数据流
|
// 监听音视频数据流
|
||||||
void _startListenTalkData() {
|
void _startListenTalkData() {
|
||||||
state.talkDataRepository.talkDataStream.listen((TalkData talkData) async {
|
// 防止重复监听
|
||||||
final contentType = talkData.contentType;
|
if (_isListening) {
|
||||||
|
AppLog.log("已经存在数据流监听,避免重复监听");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLog.log("==== 启动新的数据流监听 ====");
|
||||||
|
_isListening = true;
|
||||||
|
_streamSubscription = state.talkDataRepository.talkDataStream
|
||||||
|
.listen((TalkDataModel talkDataModel) async {
|
||||||
|
final talkData = talkDataModel.talkData;
|
||||||
|
final contentType = talkData!.contentType;
|
||||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
// 判断数据类型,进行分发处理
|
// 判断数据类型,进行分发处理
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case TalkData_ContentTypeE.G711:
|
case TalkData_ContentTypeE.G711:
|
||||||
// // 第一帧到达时记录开始时间
|
// // 第一帧到达时记录开始时间
|
||||||
// if (_isFirstAudioFrame) {
|
if (_isFirstAudioFrame) {
|
||||||
// _startAudioTime = currentTime;
|
_startAudioTime = currentTime;
|
||||||
// _isFirstAudioFrame = false;
|
_isFirstAudioFrame = false;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 计算音频延迟
|
// 计算音频延迟
|
||||||
final expectedTime = _startAudioTime + talkData.durationMs;
|
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||||
@ -129,80 +135,16 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
_playAudioFrames();
|
_playAudioFrames();
|
||||||
break;
|
break;
|
||||||
case TalkData_ContentTypeE.Image:
|
case TalkData_ContentTypeE.Image:
|
||||||
// 第一帧到达时记录开始时间
|
// 固定长度缓冲区,最多保留bufferSize帧
|
||||||
if (_isFirstFrame) {
|
|
||||||
_startTime = currentTime;
|
|
||||||
_isFirstFrame = false;
|
|
||||||
AppLog.log('第一帧帧的时间戳:${talkData.durationMs}');
|
|
||||||
}
|
|
||||||
// AppLog.log('其他帧的时间戳:${talkData.durationMs}');
|
|
||||||
// 计算帧间间隔
|
|
||||||
if (_lastFrameTimestamp != 0) {
|
|
||||||
final int frameInterval = talkData.durationMs - _lastFrameTimestamp;
|
|
||||||
_adjustBufferSize(frameInterval); // 根据帧间间隔调整缓冲区
|
|
||||||
}
|
|
||||||
_lastFrameTimestamp = talkData.durationMs; // 更新上一帧时间戳
|
|
||||||
|
|
||||||
// 然后添加到播放缓冲区
|
|
||||||
if (state.videoBuffer.length >= bufferSize) {
|
|
||||||
state.videoBuffer.removeAt(0);
|
|
||||||
}
|
|
||||||
state.videoBuffer.add(talkData);
|
state.videoBuffer.add(talkData);
|
||||||
// 先进行解码和缓存
|
if (state.videoBuffer.length > bufferSize) {
|
||||||
await _decodeAndCacheFrame(talkData);
|
state.videoBuffer.removeAt(0); // 移除最旧帧
|
||||||
// 最后尝试播放
|
}
|
||||||
_playVideoFrames();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改:视频帧播放逻辑
|
|
||||||
void _playVideoFrames() {
|
|
||||||
// 如果缓冲区为空或未达到目标大小,不进行播放
|
|
||||||
if (state.videoBuffer.isEmpty || state.videoBuffer.length < bufferSize) {
|
|
||||||
// AppLog.log('📊 缓冲中 - 当前缓冲区大小: ${state.videoBuffer.length}/${bufferSize}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 找出时间戳最小的帧(最旧的帧)
|
|
||||||
TalkData? oldestFrame;
|
|
||||||
int oldestIndex = -1;
|
|
||||||
for (int i = 0; i < state.videoBuffer.length; i++) {
|
|
||||||
if (oldestFrame == null ||
|
|
||||||
state.videoBuffer[i].durationMs < oldestFrame.durationMs) {
|
|
||||||
oldestFrame = state.videoBuffer[i];
|
|
||||||
oldestIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 确保找到了有效帧
|
|
||||||
if (oldestFrame != null && oldestIndex != -1) {
|
|
||||||
final cacheKey = oldestFrame.content.hashCode.toString();
|
|
||||||
|
|
||||||
// 使用缓存的解码图片更新显示
|
|
||||||
if (_imageCache.containsKey(cacheKey)) {
|
|
||||||
state.currentImage.value = _imageCache[cacheKey];
|
|
||||||
state.listData.value = Uint8List.fromList(oldestFrame.content);
|
|
||||||
state.videoBuffer.removeAt(oldestIndex); // 移除已播放的帧
|
|
||||||
|
|
||||||
// // 更新帧率计算
|
|
||||||
// _frameCount++;
|
|
||||||
// final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
// final elapsed = currentTime - _lastFpsUpdateTime;
|
|
||||||
//
|
|
||||||
// if (elapsed >= 1000) {
|
|
||||||
// // 每秒更新一次
|
|
||||||
// state.fps.value = (_frameCount * 1000 / elapsed).round();
|
|
||||||
// _frameCount = 0;
|
|
||||||
// _lastFpsUpdateTime = currentTime;
|
|
||||||
// }
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey');
|
|
||||||
state.videoBuffer.removeAt(oldestIndex); // 移除无法播放的帧
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增:音频帧播放逻辑
|
// 新增:音频帧播放逻辑
|
||||||
void _playAudioFrames() {
|
void _playAudioFrames() {
|
||||||
// 如果缓冲区为空或未达到目标大小,不进行播放
|
// 如果缓冲区为空或未达到目标大小,不进行播放
|
||||||
@ -233,50 +175,6 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:解码和缓存帧的方法
|
|
||||||
Future<void> _decodeAndCacheFrame(TalkData talkData) async {
|
|
||||||
try {
|
|
||||||
String cacheKey = talkData.content.hashCode.toString();
|
|
||||||
|
|
||||||
// 如果该帧还没有被缓存,则进行解码和缓存
|
|
||||||
if (!_imageCache.containsKey(cacheKey)) {
|
|
||||||
final Uint8List uint8Data = Uint8List.fromList(talkData.content);
|
|
||||||
final ui.Image image = await decodeImageFromList(uint8Data);
|
|
||||||
|
|
||||||
// 管理缓存大小
|
|
||||||
if (_imageCache.length >= bufferSize) {
|
|
||||||
_imageCache.remove(_imageCache.keys.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加到缓存
|
|
||||||
_imageCache[cacheKey] = image;
|
|
||||||
|
|
||||||
// AppLog.log('📥 缓存新帧 - 缓存数: ${_imageCache.length}, Key: $cacheKey');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
AppLog.log('❌ 帧解码错误: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增:动态调整缓冲区大小的方法
|
|
||||||
void _adjustBufferSize(int frameInterval) {
|
|
||||||
const int frameDuration = 83; // 假设每帧的时间间隔为 83ms(12fps)
|
|
||||||
const int delayThresholdHigh = frameDuration * 2; // 高延迟阈值(2帧时间)
|
|
||||||
const int delayThresholdLow = frameDuration; // 低延迟阈值(1帧时间)
|
|
||||||
const int adjustInterval = 1; // 每次调整1帧
|
|
||||||
|
|
||||||
if (frameInterval > delayThresholdHigh && bufferSize < maxBufferSize) {
|
|
||||||
// 帧间间隔较大,增加缓冲区
|
|
||||||
bufferSize = min(bufferSize + adjustInterval, maxBufferSize);
|
|
||||||
AppLog.log('📈 增加缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms');
|
|
||||||
} else if (frameInterval < delayThresholdLow &&
|
|
||||||
bufferSize > minBufferSize) {
|
|
||||||
// 帧间间隔较小,减少缓冲区
|
|
||||||
bufferSize = max(bufferSize - adjustInterval, minBufferSize);
|
|
||||||
AppLog.log('📉 减少缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 监听对讲状态
|
/// 监听对讲状态
|
||||||
void _startListenTalkStatus() {
|
void _startListenTalkStatus() {
|
||||||
state.startChartTalkStatus.statusStream.listen((talkStatus) {
|
state.startChartTalkStatus.statusStream.listen((talkStatus) {
|
||||||
@ -382,20 +280,6 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
|
||||||
Future<void> _getLockNetToken() async {
|
|
||||||
final LockNetTokenEntity entity = await ApiRepository.to.getLockNetToken(
|
|
||||||
lockId: lockDetailState.keyInfos.value.lockId.toString());
|
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
|
||||||
lockDetailState.lockNetToken = entity.data!.token!.toString();
|
|
||||||
AppLog.log('从服务器获取联网token:${lockDetailState.lockNetToken}');
|
|
||||||
} else {
|
|
||||||
BuglyTool.uploadException(
|
|
||||||
message: '点击了需要联网开锁', detail: '点击了需要联网开锁 获取连网token失败', upload: true);
|
|
||||||
showToast('网络访问失败,请检查网络是否正常'.tr, something: () {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取权限状态
|
/// 获取权限状态
|
||||||
Future<bool> getPermissionStatus() async {
|
Future<bool> getPermissionStatus() async {
|
||||||
final Permission permission = Permission.microphone;
|
final Permission permission = Permission.microphone;
|
||||||
@ -498,6 +382,31 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
requestPermissions();
|
requestPermissions();
|
||||||
|
|
||||||
|
// 启动视频渲染定时器(10fps)
|
||||||
|
videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||||
|
final int now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (state.videoBuffer.isNotEmpty) {
|
||||||
|
final TalkData oldestFrame = state.videoBuffer.removeAt(0);
|
||||||
|
if (oldestFrame.content.isNotEmpty) {
|
||||||
|
state.listData.value = Uint8List.fromList(oldestFrame.content); // 备份原始数据
|
||||||
|
final int decodeStart = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
decodeImageFromList(Uint8List.fromList(oldestFrame.content)).then((ui.Image img) {
|
||||||
|
final int decodeEnd = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
state.currentImage.value = img;
|
||||||
|
_renderedFrameCount++;
|
||||||
|
// 每秒统计一次fps
|
||||||
|
if (now - _lastFpsPrintTime >= 1000) {
|
||||||
|
// print('实际渲染fps: $_renderedFrameCount');
|
||||||
|
_renderedFrameCount = 0;
|
||||||
|
_lastFpsPrintTime = now;
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
print('图片解码失败: $e');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果缓冲区为空,不做任何操作,保持上一次内容
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -512,10 +421,17 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
stopProcessingAudio();
|
stopProcessingAudio();
|
||||||
// 清理图片缓存
|
// 清理图片缓存
|
||||||
_imageCache.clear();
|
// _imageCache.clear();
|
||||||
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||||
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
||||||
state.oneMinuteTime.value = 0;
|
state.oneMinuteTime.value = 0;
|
||||||
|
// 取消数据流监听
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
_isListening = false;
|
||||||
|
|
||||||
|
// 释放视频渲染定时器
|
||||||
|
videoRenderTimer?.cancel();
|
||||||
|
videoRenderTimer = null;
|
||||||
|
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
@ -525,6 +441,9 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
stopProcessingAudio();
|
stopProcessingAudio();
|
||||||
// 重置期望数据
|
// 重置期望数据
|
||||||
StartChartManage().reSetDefaultTalkExpect();
|
StartChartManage().reSetDefaultTalkExpect();
|
||||||
|
// 释放视频渲染定时器
|
||||||
|
videoRenderTimer?.cancel();
|
||||||
|
videoRenderTimer = null;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,34 +516,40 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
// 远程开锁
|
// 远程开锁
|
||||||
Future<void> remoteOpenLock() async {
|
Future<void> remoteOpenLock() async {
|
||||||
|
final LockListInfoItemEntity currentKeyInfo =
|
||||||
|
CommonDataManage().currentKeyInfo;
|
||||||
|
|
||||||
|
var lockId = currentKeyInfo.lockId ?? 0;
|
||||||
|
var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0;
|
||||||
|
|
||||||
final lockPeerId = StartChartManage().lockPeerId;
|
final lockPeerId = StartChartManage().lockPeerId;
|
||||||
final lockListPeerId = StartChartManage().lockListPeerId;
|
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||||
int lockId = lockDetailState.keyInfos.value.lockId ?? 0;
|
await Storage.getLockMainListData();
|
||||||
|
if (lockListInfoGroupEntity != null) {
|
||||||
// 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId
|
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||||
// 从列表中遍历出对应的peerId
|
final lockList = element.lockList;
|
||||||
lockListPeerId.forEach((element) {
|
if (lockList != null && lockList.length != 0) {
|
||||||
if (element.network?.peerId == lockPeerId) {
|
for (var lockInfo in lockList) {
|
||||||
lockId = element.lockId ?? 0;
|
final peerId = lockInfo.network?.peerId;
|
||||||
}
|
if (peerId != null && peerId != '') {
|
||||||
});
|
if (peerId == lockPeerId) {
|
||||||
|
lockId = lockInfo.lockId ?? 0;
|
||||||
final LockSetInfoEntity lockSetInfoEntity =
|
remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0;
|
||||||
await ApiRepository.to.getLockSettingInfoData(
|
}
|
||||||
lockId: lockId.toString(),
|
}
|
||||||
);
|
}
|
||||||
if (lockSetInfoEntity.errorCode!.codeIsSuccessful) {
|
|
||||||
if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 &&
|
|
||||||
lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) {
|
|
||||||
final LoginEntity entity = await ApiRepository.to
|
|
||||||
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
|
||||||
showToast('已开锁'.tr);
|
|
||||||
StartChartManage().lockListPeerId = [];
|
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
showToast('该锁的远程开锁功能未启用'.tr);
|
}
|
||||||
|
if (remoteUnlock == 1) {
|
||||||
|
final LoginEntity entity = await ApiRepository.to
|
||||||
|
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
||||||
|
if (entity.errorCode!.codeIsSuccessful) {
|
||||||
|
showToast('已开锁'.tr);
|
||||||
|
StartChartManage().lockListPeerId = [];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
showToast('该锁的远程开锁功能未启用'.tr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import 'package:star_lock/talk/call/callTalk.dart';
|
|||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart';
|
import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart';
|
import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
final TalkViewLogic logic = Get.put(TalkViewLogic());
|
final TalkViewLogic logic = Get.put(TalkViewLogic());
|
||||||
final TalkViewState state = Get.find<TalkViewLogic>().state;
|
final TalkViewState state = Get.find<TalkViewLogic>().state;
|
||||||
late Stream<int> _latencyStream;
|
late Stream<int> _latencyStream;
|
||||||
|
final startChartManage = StartChartManage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -96,60 +98,55 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Obx(
|
// 全屏背景图片或渐变色背景
|
||||||
() {
|
Obx(() {
|
||||||
final double screenWidth = MediaQuery.of(context).size.width;
|
if (state.listData.value.isEmpty) {
|
||||||
final double screenHeight = MediaQuery.of(context).size.height;
|
return SizedBox.expand(
|
||||||
|
child: Image.asset(
|
||||||
final double logicalWidth = MediaQuery.of(context).size.width;
|
'images/main/monitorBg.png',
|
||||||
final double logicalHeight = MediaQuery.of(context).size.height;
|
fit: BoxFit.cover,
|
||||||
final double devicePixelRatio =
|
),
|
||||||
MediaQuery.of(context).devicePixelRatio;
|
);
|
||||||
|
}
|
||||||
// 计算物理像素值
|
final int videoW = startChartManage.videoWidth;
|
||||||
final double physicalWidth = logicalWidth * devicePixelRatio;
|
final int videoH = startChartManage.videoHeight;
|
||||||
final double physicalHeight = logicalHeight * devicePixelRatio;
|
if (videoW == 320 && videoH == 240) {
|
||||||
|
return SizedBox.expand(
|
||||||
// 旋转后的图片尺寸
|
child: Container(
|
||||||
const int rotatedImageWidth = 480; // 原始高度
|
decoration: const BoxDecoration(
|
||||||
const int rotatedImageHeight = 864; // 原始宽度
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
// 计算缩放比例
|
end: Alignment.bottomCenter,
|
||||||
final double scaleWidth = physicalWidth / rotatedImageWidth;
|
colors: [
|
||||||
final double scaleHeight = physicalHeight / rotatedImageHeight;
|
Color(0xFF232526),
|
||||||
max(scaleWidth, scaleHeight); // 选择较大的缩放比例
|
Color(0xFF414345),
|
||||||
|
],
|
||||||
return state.listData.value.isEmpty
|
),
|
||||||
? Image.asset(
|
),
|
||||||
'images/main/monitorBg.png',
|
),
|
||||||
width: screenWidth,
|
);
|
||||||
height: screenHeight,
|
}
|
||||||
fit: BoxFit.cover,
|
return const SizedBox.shrink();
|
||||||
)
|
}),
|
||||||
: PopScope(
|
// 视频窗口,分辨率判断
|
||||||
canPop: false,
|
Obx(() {
|
||||||
child: RepaintBoundary(
|
if (state.listData.value.isEmpty) {
|
||||||
key: state.globalKey,
|
return const SizedBox.shrink();
|
||||||
child: SizedBox.expand(
|
}
|
||||||
child: RotatedBox(
|
final int videoW = startChartManage.videoWidth;
|
||||||
quarterTurns: -1,
|
final int videoH = startChartManage.videoHeight;
|
||||||
child: Obx(
|
if (videoW == 320 && videoH == 240) {
|
||||||
() => state.currentImage.value != null
|
return Positioned(
|
||||||
? RawImage(
|
top: 150.h,
|
||||||
image: state.currentImage.value,
|
left: 0,
|
||||||
width: ScreenUtil().scaleWidth,
|
right: 0,
|
||||||
height: ScreenUtil().scaleHeight,
|
child: _buildVideoWidget(),
|
||||||
fit: BoxFit.cover,
|
);
|
||||||
filterQuality: FilterQuality.high,
|
} else {
|
||||||
)
|
// 直接全屏显示
|
||||||
: Container(color: Colors.transparent),
|
return _buildVideoWidget();
|
||||||
),
|
}
|
||||||
),
|
}),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Obx(() => state.listData.value.isEmpty
|
Obx(() => state.listData.value.isEmpty
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 310.h,
|
bottom: 310.h,
|
||||||
@ -158,35 +155,35 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
||||||
))
|
))
|
||||||
: Container()),
|
: Container()),
|
||||||
Obx(
|
Obx(() =>
|
||||||
() => state.listData.value.isNotEmpty &&
|
state.listData.value.isNotEmpty && state.oneMinuteTime.value > 0
|
||||||
state.oneMinuteTime.value > 0
|
? Positioned(
|
||||||
? Positioned(
|
top: ScreenUtil().statusBarHeight + 75.h,
|
||||||
top: ScreenUtil().statusBarHeight + 75.h,
|
width: 1.sw,
|
||||||
width: 1.sw,
|
child: Obx(
|
||||||
child: Obx(
|
() {
|
||||||
() {
|
final String sec = (state.oneMinuteTime.value % 60)
|
||||||
final String sec = (state.oneMinuteTime.value % 60)
|
.toString()
|
||||||
.toString()
|
.padLeft(2, '0');
|
||||||
.padLeft(2, '0');
|
final String min = (state.oneMinuteTime.value ~/ 60)
|
||||||
final String min = (state.oneMinuteTime.value ~/ 60)
|
.toString()
|
||||||
.toString()
|
.padLeft(2, '0');
|
||||||
.padLeft(2, '0');
|
return Row(
|
||||||
return Row(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
Text(
|
||||||
Text(
|
'$min:$sec',
|
||||||
'$min:$sec',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 26.sp, color: Colors.white),
|
||||||
fontSize: 26.sp, color: Colors.white),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
)
|
||||||
)
|
: Container()),
|
||||||
: Container(),
|
|
||||||
),
|
/// 工具栏
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 10.w,
|
bottom: 10.w,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -456,7 +453,6 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
// if (state.talkStatus.value == TalkStatus.answeredSuccessfully &&
|
// if (state.talkStatus.value == TalkStatus.answeredSuccessfully &&
|
||||||
// state.listData.value.length > 0) {
|
// state.listData.value.length > 0) {
|
||||||
// logic.udpOpenDoorAction();
|
// logic.udpOpenDoorAction();
|
||||||
logic.remoteOpenLock();
|
|
||||||
// }
|
// }
|
||||||
// if (UDPManage().remoteUnlock == 1) {
|
// if (UDPManage().remoteUnlock == 1) {
|
||||||
// logic.udpOpenDoorAction();
|
// logic.udpOpenDoorAction();
|
||||||
@ -464,6 +460,7 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
// } else {
|
// } else {
|
||||||
// logic.showToast('请在锁设置中开启远程开锁'.tr);
|
// logic.showToast('请在锁设置中开启远程开锁'.tr);
|
||||||
// }
|
// }
|
||||||
|
logic.remoteOpenLock();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
@ -618,4 +615,68 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
// UdpTalkDataHandler().resetDataRates();
|
// UdpTalkDataHandler().resetDataRates();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildVideoWidget() {
|
||||||
|
// 工具栏宽度
|
||||||
|
double barWidth = 1.sw - 30.w * 2;
|
||||||
|
int videoW = startChartManage.videoWidth;
|
||||||
|
int videoH = startChartManage.videoHeight;
|
||||||
|
int quarterTurns = startChartManage.rotateAngle ~/ 90;
|
||||||
|
bool isRotated = quarterTurns % 2 == 1;
|
||||||
|
// 旋转后宽高互换
|
||||||
|
double videoAspect = isRotated ? videoW / videoH : videoH / videoW;
|
||||||
|
double containerHeight =
|
||||||
|
barWidth * (isRotated ? videoW / videoH : videoH / videoW);
|
||||||
|
|
||||||
|
if (videoW == 320 && videoH == 240) {
|
||||||
|
return Center(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20.h),
|
||||||
|
child: Container(
|
||||||
|
width: barWidth,
|
||||||
|
height: containerHeight,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF232526),
|
||||||
|
Color(0xFF414345),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: quarterTurns,
|
||||||
|
child: RawImage(
|
||||||
|
image: state.currentImage.value,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
width: barWidth,
|
||||||
|
height: containerHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: RepaintBoundary(
|
||||||
|
key: state.globalKey,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||||
|
child: RawImage(
|
||||||
|
image: state.currentImage.value,
|
||||||
|
width: ScreenUtil().scaleWidth,
|
||||||
|
height: ScreenUtil().scaleHeight,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,6 +90,5 @@ class TalkViewState {
|
|||||||
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||||
RxBool hasAudioData = false.obs; // 是否有音频数据
|
RxBool hasAudioData = false.obs; // 是否有音频数据
|
||||||
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
||||||
// 添加图片状态变量
|
Rx<ui.Image?> currentImage = Rx<ui.Image?>(null);
|
||||||
final Rx<ui.Image?> currentImage = Rx<ui.Image?>(null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,9 @@ import 'package:star_lock/network/api_repository.dart';
|
|||||||
import 'package:star_lock/talk/call/g711.dart';
|
import 'package:star_lock/talk/call/g711.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
@ -44,14 +46,20 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
// 添加模拟数据相关变量
|
// 添加模拟数据相关变量
|
||||||
static const int CHUNK_SIZE = 4096;
|
static const int CHUNK_SIZE = 4096;
|
||||||
Timer? _mockDataTimer;
|
Timer? _mockDataTimer;
|
||||||
|
int _startAudioTime = 0; // 开始播放时间戳
|
||||||
|
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||||
|
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||||
// 定义音频帧缓冲和发送函数
|
// 定义音频帧缓冲和发送函数
|
||||||
final List<int> _bufferedAudioFrames = <int>[];
|
final List<int> _bufferedAudioFrames = <int>[];
|
||||||
final Queue<List<int>> _frameBuffer = Queue<List<int>>();
|
final Queue<List<int>> _frameBuffer = Queue<List<int>>();
|
||||||
static const int FRAME_BUFFER_SIZE = 25;
|
static const int FRAME_BUFFER_SIZE = 25;
|
||||||
|
|
||||||
|
// 添加监听状态和订阅引用
|
||||||
|
bool _isListening = false;
|
||||||
|
StreamSubscription? _streamSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
|
||||||
// 初始化 WebView 控制器
|
// 初始化 WebView 控制器
|
||||||
state.webViewController = WebViewController()
|
state.webViewController = WebViewController()
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
@ -63,18 +71,40 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
state.isShowLoading.value = true;
|
super.onInit();
|
||||||
// 加载本地 HTML
|
|
||||||
_loadLocalHtml();
|
|
||||||
// 创建流数据监听
|
// 创建流数据监听
|
||||||
_createFramesStreamListen();
|
_createFramesStreamListen();
|
||||||
// playLocalTestVideo();
|
|
||||||
_startListenTalkStatus();
|
_startListenTalkStatus();
|
||||||
state.talkStatus.value = state.startChartTalkStatus.status;
|
state.talkStatus.value = state.startChartTalkStatus.status;
|
||||||
// 初始化音频播放器
|
// 初始化音频播放器
|
||||||
_initFlutterPcmSound();
|
_initFlutterPcmSound();
|
||||||
|
|
||||||
// 初始化录音控制器
|
// 初始化录音控制器
|
||||||
_initAudioRecorder();
|
_initAudioRecorder();
|
||||||
|
|
||||||
|
// 加载本地 HTML
|
||||||
|
_loadLocalHtml();
|
||||||
|
|
||||||
|
// playLocalTestVideo();
|
||||||
|
|
||||||
|
requestPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> requestPermissions() async {
|
||||||
|
// 申请存储权限
|
||||||
|
var storageStatus = await Permission.storage.request();
|
||||||
|
// 申请录音权限
|
||||||
|
var microphoneStatus = await Permission.microphone.request();
|
||||||
|
|
||||||
|
if (storageStatus.isGranted && microphoneStatus.isGranted) {
|
||||||
|
print("Permissions granted");
|
||||||
|
} else {
|
||||||
|
print("Permissions denied");
|
||||||
|
// 如果权限被拒绝,可以提示用户或跳转到设置页面
|
||||||
|
if (await Permission.storage.isPermanentlyDenied) {
|
||||||
|
openAppSettings(); // 跳转到应用设置页面
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化音频录制器
|
/// 初始化音频录制器
|
||||||
@ -96,16 +126,66 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _createFramesStreamListen() async {
|
void _createFramesStreamListen() async {
|
||||||
state.talkDataRepository.talkDataStream.listen((TalkData event) async {
|
// 防止重复监听
|
||||||
// 添加新帧到缓冲区
|
if (_isListening) {
|
||||||
_frameBuffer.add(event.content);
|
AppLog.log("已经存在数据流监听,避免重复监听");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 当缓冲区超过最大容量时,发送最早的帧并移除
|
AppLog.log("==== 启动新的数据流监听 ====");
|
||||||
while (_frameBuffer.length > FRAME_BUFFER_SIZE) {
|
_isListening = true;
|
||||||
if (_frameBuffer.isNotEmpty) {
|
_streamSubscription = state.talkDataRepository.talkDataStream
|
||||||
final frame = _frameBuffer.removeFirst();
|
.listen((TalkDataModel talkDataModel) async {
|
||||||
await _sendBufferedData(frame);
|
final talkData = talkDataModel.talkData;
|
||||||
}
|
final contentType = talkData!.contentType;
|
||||||
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 判断数据类型,进行分发处理
|
||||||
|
switch (contentType) {
|
||||||
|
case TalkData_ContentTypeE.G711:
|
||||||
|
if (state.isShowLoading.isFalse) {
|
||||||
|
// // 第一帧到达时记录开始时间
|
||||||
|
if (_isFirstAudioFrame) {
|
||||||
|
_startAudioTime = currentTime;
|
||||||
|
_isFirstAudioFrame = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算音频延迟
|
||||||
|
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||||
|
final audioDelay = currentTime - expectedTime;
|
||||||
|
|
||||||
|
// 如果延迟太大,清空缓冲区并直接播放
|
||||||
|
if (audioDelay > 500) {
|
||||||
|
state.audioBuffer.clear();
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
_playAudioFrames();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.audioBuffer.length >= audioBufferSize) {
|
||||||
|
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||||
|
}
|
||||||
|
state.audioBuffer.add(talkData); // 添加新数据
|
||||||
|
// 添加音频播放逻辑,与视频类似
|
||||||
|
_playAudioFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case TalkData_ContentTypeE.H264:
|
||||||
|
// // 添加新帧到缓冲区
|
||||||
|
_frameBuffer.add(talkData.content);
|
||||||
|
|
||||||
|
// 当缓冲区超过最大容量时,发送最早的帧并移除
|
||||||
|
while (_frameBuffer.length > FRAME_BUFFER_SIZE) {
|
||||||
|
if (_frameBuffer.isNotEmpty) {
|
||||||
|
final frame = _frameBuffer.removeFirst();
|
||||||
|
await _sendBufferedData(frame);
|
||||||
|
}
|
||||||
|
if (state.isShowLoading.isTrue) {
|
||||||
|
state.isShowLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -134,6 +214,51 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// 新增:音频帧播放逻辑
|
||||||
|
void _playAudioFrames() {
|
||||||
|
// 如果缓冲区为空或未达到目标大小,不进行播放
|
||||||
|
// 音频缓冲区要求更小,以减少延迟
|
||||||
|
if (state.audioBuffer.isEmpty ||
|
||||||
|
state.audioBuffer.length < audioBufferSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找出时间戳最小的音频帧
|
||||||
|
TalkData? oldestFrame;
|
||||||
|
int oldestIndex = -1;
|
||||||
|
for (int i = 0; i < state.audioBuffer.length; i++) {
|
||||||
|
if (oldestFrame == null ||
|
||||||
|
state.audioBuffer[i].durationMs < oldestFrame.durationMs) {
|
||||||
|
oldestFrame = state.audioBuffer[i];
|
||||||
|
oldestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保找到了有效帧
|
||||||
|
if (oldestFrame != null && oldestIndex != -1) {
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
// 播放音频
|
||||||
|
_playAudioData(oldestFrame);
|
||||||
|
}
|
||||||
|
state.audioBuffer.removeAt(oldestIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 播放音频数据
|
||||||
|
void _playAudioData(TalkData talkData) async {
|
||||||
|
if (state.isOpenVoice.value) {
|
||||||
|
final list =
|
||||||
|
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
|
||||||
|
// // 将 PCM 数据转换为 PcmArrayInt16
|
||||||
|
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
||||||
|
FlutterPcmSound.feed(fromList);
|
||||||
|
if (!state.isPlaying.value) {
|
||||||
|
FlutterPcmSound.play();
|
||||||
|
state.isPlaying.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 加载html文件
|
/// 加载html文件
|
||||||
Future<void> _loadLocalHtml() async {
|
Future<void> _loadLocalHtml() async {
|
||||||
// 加载 HTML 文件内容
|
// 加载 HTML 文件内容
|
||||||
@ -186,10 +311,10 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||||
if (state.isShowLoading.isFalse) {
|
if (state.isShowLoading.isFalse) {
|
||||||
state.oneMinuteTime.value++;
|
state.oneMinuteTime.value++;
|
||||||
if (state.oneMinuteTime.value >= 60) {
|
// if (state.oneMinuteTime.value >= 60) {
|
||||||
t.cancel(); // 取消定时器
|
// t.cancel(); // 取消定时器
|
||||||
state.oneMinuteTime.value = 0;
|
// state.oneMinuteTime.value = 0;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -321,7 +446,7 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 首先应用固定增益提升基础音量
|
// 首先应用固定增益提升基础音量
|
||||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
List<int> amplifiedFrame = _applyGain(frame, 1.8);
|
||||||
// 编码为G711数据
|
// 编码为G711数据
|
||||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||||
_bufferedAudioFrames.addAll(encodedData);
|
_bufferedAudioFrames.addAll(encodedData);
|
||||||
@ -409,7 +534,7 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final LockSetInfoEntity lockSetInfoEntity =
|
final LockSetInfoEntity lockSetInfoEntity =
|
||||||
await ApiRepository.to.getLockSettingInfoData(
|
await ApiRepository.to.getLockSettingInfoData(
|
||||||
lockId: lockId.toString(),
|
lockId: lockId.toString(),
|
||||||
);
|
);
|
||||||
if (lockSetInfoEntity.errorCode!.codeIsSuccessful) {
|
if (lockSetInfoEntity.errorCode!.codeIsSuccessful) {
|
||||||
@ -427,6 +552,38 @@ class H264WebViewLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 停止播放音频
|
||||||
|
void _stopPlayG711Data() async {
|
||||||
|
await FlutterPcmSound.pause();
|
||||||
|
await FlutterPcmSound.stop();
|
||||||
|
await FlutterPcmSound.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_stopPlayG711Data(); // 停止播放音频
|
||||||
|
|
||||||
|
state.audioBuffer.clear(); // 清空音频缓冲区
|
||||||
|
|
||||||
|
state.oneMinuteTimeTimer?.cancel();
|
||||||
|
state.oneMinuteTimeTimer = null;
|
||||||
|
|
||||||
|
// 停止播放音频
|
||||||
|
stopProcessingAudio();
|
||||||
|
|
||||||
|
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||||
|
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
||||||
|
state.oneMinuteTime.value = 0;
|
||||||
|
|
||||||
|
// 取消数据流监听
|
||||||
|
_streamSubscription?.cancel();
|
||||||
|
_isListening = false;
|
||||||
|
|
||||||
|
// 重置期望数据
|
||||||
|
StartChartManage().reSetDefaultTalkExpect();
|
||||||
|
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/app_settings/app_colors.dart';
|
import 'package:star_lock/app_settings/app_colors.dart';
|
||||||
import 'package:star_lock/app_settings/app_settings.dart';
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/talk/call/callTalk.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
||||||
@ -25,6 +26,7 @@ class _H264WebViewState extends State<H264WebView>
|
|||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
final H264WebViewLogic logic = Get.put(H264WebViewLogic());
|
final H264WebViewLogic logic = Get.put(H264WebViewLogic());
|
||||||
final H264WebViewState state = Get.find<H264WebViewLogic>().state;
|
final H264WebViewState state = Get.find<H264WebViewLogic>().state;
|
||||||
|
final startChartManage = StartChartManage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -71,8 +73,13 @@ class _H264WebViewState extends State<H264WebView>
|
|||||||
height: screenHeight,
|
height: screenHeight,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: WebViewWidget(
|
: SizedBox.expand(
|
||||||
controller: state.webViewController,
|
child: RotatedBox(
|
||||||
|
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||||
|
child: WebViewWidget(
|
||||||
|
controller: state.webViewController,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Obx(
|
Obx(
|
||||||
@ -411,10 +418,13 @@ class _H264WebViewState extends State<H264WebView>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
state.animationController.dispose(); // 确保释放控制器
|
state.animationController.dispose();
|
||||||
super.dispose();
|
|
||||||
|
|
||||||
|
CallTalk().finishAVData();
|
||||||
|
// UdpTalkDataHandler().resetDataRates();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
||||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
@ -49,4 +50,6 @@ class H264WebViewState {
|
|||||||
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
||||||
RxBool hasAudioData = false.obs; // 是否有音频数据
|
RxBool hasAudioData = false.obs; // 是否有音频数据
|
||||||
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
||||||
|
List<TalkData> audioBuffer = <TalkData>[].obs;
|
||||||
|
RxBool isPlaying = false.obs; // 是否开始播放
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:event_bus/event_bus.dart';
|
import 'package:event_bus/event_bus.dart';
|
||||||
|
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
|
|
||||||
import '../main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
import '../main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
||||||
|
|
||||||
@ -195,6 +196,7 @@ class RogerThatLockInfoDataEvent {
|
|||||||
class GetGatewayListRefreshUI {
|
class GetGatewayListRefreshUI {
|
||||||
GetGatewayListRefreshUI();
|
GetGatewayListRefreshUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同意隐私协议
|
/// 同意隐私协议
|
||||||
class AgreePrivacyAgreement {
|
class AgreePrivacyAgreement {
|
||||||
AgreePrivacyAgreement();
|
AgreePrivacyAgreement();
|
||||||
@ -204,3 +206,10 @@ class AgreePrivacyAgreement {
|
|||||||
class SuccessfulDistributionNetwork {
|
class SuccessfulDistributionNetwork {
|
||||||
SuccessfulDistributionNetwork();
|
SuccessfulDistributionNetwork();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置锁列表数据
|
||||||
|
class SetLockListInfoGroupEntity {
|
||||||
|
SetLockListInfoGroupEntity({required this.lockListInfoGroupEntity});
|
||||||
|
|
||||||
|
LockListInfoGroupEntity lockListInfoGroupEntity;
|
||||||
|
}
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
set -e
|
set -e
|
||||||
APP_PRODUCT_NAME=$APP_PRODUCT_NAME
|
APP_PRODUCT_NAME=$APP_PRODUCT_NAME
|
||||||
BUILD_STATUS=$1
|
BUILD_STATUS=$1
|
||||||
if [[ "${CI_COMMIT_BRANCH}" == "release" ]] ; then
|
if [[ "${CI_COMMIT_BRANCH}" == "release_sky" ]] ; then
|
||||||
WECAHT_WEBHOOK_URL=$PRE_QYWECAHT_WEBHOOK_URL
|
WECAHT_WEBHOOK_URL=$PRE_QYWECAHT_WEBHOOK_URL
|
||||||
SKY_IOS_DOWNLOAD_URL=$PRE_SKY_IOS_DOWNLOAD_URL
|
SKY_IOS_DOWNLOAD_URL=$PRE_SKY_IOS_DOWNLOAD_URL
|
||||||
SKY_ANDROID_DOWNLOAD_URL=$PRE_SKY_ANDROID_DOWNLOAD_URL
|
SKY_ANDROID_DOWNLOAD_URL=$PRE_SKY_ANDROID_DOWNLOAD_URL
|
||||||
XHJ_IOS_DOWNLOAD_URL=$PRE_XHJ_IOS_DOWNLOAD_URL
|
XHJ_IOS_DOWNLOAD_URL=$PRE_XHJ_IOS_DOWNLOAD_URL
|
||||||
XHJ_ANDROID_DOWNLOAD_URL=$PRE_XHJ_ANDROID_DOWNLOAD_URL
|
XHJ_ANDROID_DOWNLOAD_URL=$PRE_XHJ_ANDROID_DOWNLOAD_URL
|
||||||
elif [[ "${CI_COMMIT_BRANCH}" == "develop" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops" ]]; then
|
elif [[ "${CI_COMMIT_BRANCH}" == "develop_sky" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops_sky" ]]; then
|
||||||
WECAHT_WEBHOOK_URL=$DEV_QYWECAHT_WEBHOOK_URL
|
WECAHT_WEBHOOK_URL=$DEV_QYWECAHT_WEBHOOK_URL
|
||||||
SKY_IOS_DOWNLOAD_URL=$DEV_SKY_IOS_DOWNLOAD_URL
|
SKY_IOS_DOWNLOAD_URL=$DEV_SKY_IOS_DOWNLOAD_URL
|
||||||
SKY_ANDROID_DOWNLOAD_URL=$DEV_SKY_ANDROID_DOWNLOAD_URL
|
SKY_ANDROID_DOWNLOAD_URL=$DEV_SKY_ANDROID_DOWNLOAD_URL
|
||||||
|
|||||||
@ -127,7 +127,10 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
aliyun_face_plugin:
|
aliyun_face_plugin:
|
||||||
path: aliyun_face_plugin
|
path: aliyun_face_plugin
|
||||||
|
video_decode_plugin:
|
||||||
|
git:
|
||||||
|
url: git@code.star-lock.cn:liyi/video_decode_plugin.git
|
||||||
|
ref: 68bb4b7fb637ef5a78856908e1bc464f50fe967a
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
@ -277,6 +280,8 @@ dependencies:
|
|||||||
video_thumbnail: ^0.5.3
|
video_thumbnail: ^0.5.3
|
||||||
# 角标管理
|
# 角标管理
|
||||||
flutter_app_badger: ^1.3.0
|
flutter_app_badger: ^1.3.0
|
||||||
|
# 滑块支持
|
||||||
|
slide_to_act: ^2.0.2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +1,106 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# 读取环境变量
|
# ========================
|
||||||
URL=$CI_API_V4_URL
|
# tag_generator.sh
|
||||||
TOKEN=$GITLAB_ACCESS_TOKEN
|
# 用于自动识别并递增以 _sky 结尾的 tag(如 v1.5.556_sky),并生成下一个 tag。
|
||||||
PROJECT_ID=$CI_PROJECT_ID
|
# 递增规则:feat: 提交递增 minor,fix: 或其他提交递增 patch。
|
||||||
next_tag=""
|
# ========================
|
||||||
newest_tag=""
|
|
||||||
|
# 读取环境变量(由 CI/CD 系统注入)
|
||||||
|
URL=$CI_API_V4_URL # GitLab API v4 地址
|
||||||
|
TOKEN=$GITLAB_ACCESS_TOKEN # GitLab 访问 Token
|
||||||
|
PROJECT_ID=$CI_PROJECT_ID # 当前项目 ID
|
||||||
|
next_tag="" # 新的 tag 变量
|
||||||
|
newest_tag="" # 最新 tag 变量
|
||||||
|
|
||||||
echo "PRIVATE-TOKEN: $TOKEN $URL/projects/$PROJECT_ID/repository/tags"
|
echo "PRIVATE-TOKEN: $TOKEN $URL/projects/$PROJECT_ID/repository/tags"
|
||||||
|
# 获取项目所有 tag 的 json 列表
|
||||||
|
# 需要 jq 工具解析 json
|
||||||
|
# tags_json 是所有 tag 的原始 json 数据
|
||||||
tags_json=$(curl -H "Content-Type: application/json" -H "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/tags")
|
tags_json=$(curl -H "Content-Type: application/json" -H "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/tags")
|
||||||
#echo "tags_json:$tags_json\n"
|
# 提取所有 tag 名称,按时间倒序排列(最新的在前)
|
||||||
tags=$(echo "$tags_json" | jq -r '.[].name')
|
tags=$(echo "$tags_json" | jq -r '.[].name')
|
||||||
tags_length=$(echo "$tags_json" | jq -r 'length')
|
|
||||||
if [ "$tags_length" -lt 1 ]; then
|
# 只保留以 _sky 结尾的 tag(即只处理 v1.2.3_sky 这种格式的 tag)
|
||||||
next_tag="v1.0.0"
|
sky_tags=$(echo "$tags" | grep '_sky$')
|
||||||
|
# 取最新的 _sky tag(即第一个)
|
||||||
|
newest_sky_tag=$(echo "$sky_tags" | head -n 1)
|
||||||
|
|
||||||
|
# 如果没有 _sky 结尾的 tag,则从最新的 tag 提取版本号,生成 vX.Y.Z_sky
|
||||||
|
if [ -z "$newest_sky_tag" ]; then
|
||||||
|
# 取最新的 tag(不管是否带_sky)
|
||||||
|
latest_tag=$(echo "$tags" | head -n 1)
|
||||||
|
if [ -n "$latest_tag" ]; then
|
||||||
|
# 提取版本号部分(去掉前缀v和后缀_sky等)
|
||||||
|
version_part=${latest_tag#v} # 去掉v
|
||||||
|
version_part=${version_part%_sky} # 去掉_sky(如果有)
|
||||||
|
IFS='.' read -r major minor patch <<< "$version_part"
|
||||||
|
next_tag="v$major.$minor.$patch_sky"
|
||||||
|
else
|
||||||
|
next_tag="v1.0.0_sky"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
newest_tag=$(echo "$tags" | head -n 1)
|
# 解析版本号部分(去掉 _sky 后缀和 v 前缀)
|
||||||
IFS='.' read -r major minor patch <<< "$newest_tag"
|
# 例如 v1.5.556_sky -> 1.5.556
|
||||||
major="${major#v}"
|
version_part=${newest_sky_tag%_sky} # 去掉 _sky 后缀
|
||||||
compare_json=""
|
version_part=${version_part#v} # 去掉 v 前缀
|
||||||
|
IFS='.' read -r major minor patch <<< "$version_part" # 拆分出主、次、修订号
|
||||||
|
compare_json="" # 用于存储 commit 对比结果
|
||||||
|
# 判断命令参数,决定对比范围
|
||||||
if [[ "$1" == "generate_tag" ]];then
|
if [[ "$1" == "generate_tag" ]];then
|
||||||
echo "generate_tag:$newest_tag-to-master\n"
|
# 生成 tag 时,对比最新 _sky tag 和 master 之间的提交
|
||||||
compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master")
|
echo "generate_tag:$newest_sky_tag-to-master\n"
|
||||||
|
compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_sky_tag&to=master")
|
||||||
elif [[ "$1" == "generate_version" ]]; then
|
elif [[ "$1" == "generate_version" ]]; then
|
||||||
echo "generate_version:master-to-$CI_COMMIT_BRANCH\n"
|
# 生成版本号时,对比 master 和当前分支之间的提交
|
||||||
|
echo "generate_version:master-to-$CI_COMMIT_BRANCH\n"
|
||||||
compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH")
|
compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH")
|
||||||
fi
|
fi
|
||||||
echo "compare_json:$compare_json\n"
|
echo "compare_json:$compare_json\n"
|
||||||
new_patch=$patch
|
new_patch=$patch # 新的 patch 号
|
||||||
new_minor=$minor
|
new_minor=$minor # 新的 minor 号
|
||||||
|
# 遍历所有 commit,根据提交信息递增版本号
|
||||||
while IFS= read -r commit_json; do
|
while IFS= read -r commit_json; do
|
||||||
# 使用 jq 解析每一行的 JSON 对象
|
# 解析每个 commit 的 id 和 message
|
||||||
commit_id=$(echo "$commit_json" | jq -r '.id')
|
commit_id=$(echo "$commit_json" | jq -r '.id')
|
||||||
commit_message=$(echo "$commit_json" | jq -r '.message')
|
commit_message=$(echo "$commit_json" | jq -r '.message')
|
||||||
echo "----$commit_message"
|
echo "----$commit_message"
|
||||||
|
# 如果有 feat: 类型提交,minor 递增(只递增一次)
|
||||||
if [[ "$commit_message" =~ ("feat:"*) ]] && [[ $new_minor == $minor ]]; then
|
if [[ "$commit_message" =~ ("feat:"*) ]] && [[ $new_minor == $minor ]]; then
|
||||||
((new_minor++))
|
((new_minor++))
|
||||||
# new_patch=0
|
# 如果有 fix: 类型提交,patch 递增
|
||||||
# break
|
|
||||||
elif [[ "$commit_message" =~ ("fix:"*) ]]; then
|
elif [[ "$commit_message" =~ ("fix:"*) ]]; then
|
||||||
((new_patch++))
|
((new_patch++))
|
||||||
|
# 其他类型提交(非 Merge/Revert),patch 递增
|
||||||
elif [[ ! "$commit_message" =~ ("Merge"* | "Revert"*) ]]; then
|
elif [[ ! "$commit_message" =~ ("Merge"* | "Revert"*) ]]; then
|
||||||
((new_patch++))
|
((new_patch++))
|
||||||
fi
|
fi
|
||||||
done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}')
|
done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}')
|
||||||
next_tag="v$major.$new_minor.$new_patch"
|
# 组装新的 tag,格式为 v<major>.<minor>.<patch>_sky
|
||||||
|
next_tag="v$major.$new_minor.$new_patch_sky"
|
||||||
fi
|
fi
|
||||||
echo "New Tag:$newest_tag;New version: $next_tag;command: $1"
|
|
||||||
|
echo "New Tag:$newest_sky_tag;New version: $next_tag;command: $1"
|
||||||
|
|
||||||
|
# 如果是 generate_tag 命令,且新 tag 和最新 tag 一致,则跳过生成
|
||||||
if [[ "$1" == "generate_tag" ]];then
|
if [[ "$1" == "generate_tag" ]];then
|
||||||
if [ "$next_tag" == "$newest_tag" ]; then
|
if [ "$next_tag" == "$newest_sky_tag" ]; then
|
||||||
echo "no change from master,skip to generate tag"
|
echo "no change from master,skip to generate tag"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
# 配置 git 用户名(可根据需要补充邮箱等)
|
||||||
git config user.name
|
git config user.name
|
||||||
|
# 创建并推送新 tag
|
||||||
git tag $next_tag
|
git tag $next_tag
|
||||||
git push -u origin $next_tag
|
git push -u origin $next_tag
|
||||||
echo "generate tag: $next_tag"
|
echo "generate tag: $next_tag"
|
||||||
elif [[ "$1" == "generate_version" ]]; then
|
elif [[ "$1" == "generate_version" ]]; then
|
||||||
|
# 如果是 generate_version 命令,仅导出新版本号到环境变量
|
||||||
export NEXT_VERSION="$next_tag"
|
export NEXT_VERSION="$next_tag"
|
||||||
echo "generate version: $NEXT_VERSION"
|
echo "generate version: $NEXT_VERSION"
|
||||||
fi
|
fi
|
||||||
|
# 无论哪种情况,都把新 tag 写入 app_new.version 文件
|
||||||
|
# 供后续流程使用
|
||||||
|
|
||||||
echo "$next_tag" > app_new.version
|
echo "$next_tag" > app_new.version
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user