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
|
||||
- flutter
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
- if: $CI_COMMIT_BRANCH == "release"
|
||||
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||
- 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]+)?$/
|
||||
|
||||
.notify_rule:
|
||||
@ -27,8 +27,8 @@ variables:
|
||||
- macos
|
||||
- flutter
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
- if: $CI_COMMIT_BRANCH == "release"
|
||||
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
||||
|
||||
.generate_tag_rule:
|
||||
@ -36,16 +36,16 @@ variables:
|
||||
- macos
|
||||
- flutter
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "master"
|
||||
- if: $CI_COMMIT_BRANCH == "master_sky"
|
||||
|
||||
.generate_next_version_rule:
|
||||
tags:
|
||||
- macos
|
||||
- flutter
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "develop"
|
||||
- if: $CI_COMMIT_BRANCH == "release"
|
||||
- if: $CI_COMMIT_BRANCH == "canary_release"
|
||||
- if: $CI_COMMIT_BRANCH == "develop_sky"
|
||||
- if: $CI_COMMIT_BRANCH == "release_sky"
|
||||
- if: $CI_COMMIT_BRANCH == "canary_release_sky"
|
||||
- if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/
|
||||
|
||||
.print_env:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 星锁APP
|
||||
|
||||
测试ci
|
||||
星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。
|
||||
|
||||
基于Flutter技术架构,支持Android和iOS平台。
|
||||
|
||||
@ -8,8 +8,9 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR}
|
||||
echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}"
|
||||
cd ${CI_PROJECT_DIR}/android
|
||||
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]+)?$'
|
||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then
|
||||
# 只支持 v1.2.3_sky 这种tag格式
|
||||
regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$'
|
||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then
|
||||
echo "===build canary_release: ${NEXT_VERSION}"
|
||||
export ENV_BUILD_TAG=${NEXT_VERSION}
|
||||
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_bundle flavor:xhj_bundle --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}"
|
||||
bundle exec fastlane beta flavor:xhj 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}"
|
||||
bundle exec fastlane beta flavor:xhj env:pre --verbose
|
||||
bundle exec fastlane beta flavor:sky env:pre --verbose
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
|
||||
#player {
|
||||
object-fit: cover;
|
||||
height: 56vh;
|
||||
transform: rotate(-90deg);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -9,8 +9,9 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}"
|
||||
cd ${CI_PROJECT_DIR}/ios
|
||||
#bundle exec pod install
|
||||
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]+)?$'
|
||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then
|
||||
# 只支持 v1.2.3_sky 这种tag格式
|
||||
regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$'
|
||||
if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then
|
||||
echo "===build canary_release: ${NEXT_VERSION}"
|
||||
export ENV_BUILD_TAG=${NEXT_VERSION}
|
||||
bundle exec fastlane release_ipa flavor:xhj --verbose
|
||||
@ -19,11 +20,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then
|
||||
echo "===build release===$ENV_BUILD_TAG"
|
||||
bundle exec fastlane release_ipa flavor:xhj --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}"
|
||||
bundle exec fastlane beta flavor:xhj 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}"
|
||||
bundle exec fastlane beta flavor:xhj 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/advancedFunctionRecord/advancedFunctionRecord_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/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/valueAddedServicesListSMSTemplate/customSMSTemplateList_page.dart';
|
||||
import 'starLockApplication/starLockApplication.dart';
|
||||
import 'talk/starChart/views/imageTransmission/image_transmission_page.dart';
|
||||
import 'tools/seletKeyCyclicDate/seletKeyCyclicDate_page.dart';
|
||||
|
||||
abstract class Routers {
|
||||
@ -514,6 +516,8 @@ abstract class Routers {
|
||||
static const String starChartPage = '/starChartPage'; //星图
|
||||
static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面
|
||||
static const String h264WebView = '/h264WebView'; //星图对讲页面
|
||||
static const String imageTransmissionView =
|
||||
'/imageTransmissionView'; //星图对讲页面(图传)
|
||||
}
|
||||
|
||||
abstract class AppRouters {
|
||||
@ -1184,6 +1188,13 @@ abstract class AppRouters {
|
||||
page: () => const DoubleLockLinkPage()),
|
||||
GetPage<dynamic>(
|
||||
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:get/get.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/commonDataManage.dart';
|
||||
|
||||
@ -190,10 +191,12 @@ class BlueManage {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isMatch = _isMatch(scanResult
|
||||
.advertisementData.serviceUuids
|
||||
.map((e) => e.uuid)
|
||||
.toList());
|
||||
final isMatch = _isMatch(
|
||||
scanResult.advertisementData.serviceUuids
|
||||
.map((e) => e.uuid)
|
||||
.toList(),
|
||||
isSingle: true,
|
||||
);
|
||||
|
||||
if (isMatch && (scanResult.rssi >= -100)) {
|
||||
// 查询id相同的元素
|
||||
@ -276,6 +279,7 @@ class BlueManage {
|
||||
.map((e) => e.uuid)
|
||||
.toList(),
|
||||
deviceType: deviceType,
|
||||
isSingle: false,
|
||||
);
|
||||
// 判断名字为空的直接剔除
|
||||
if (isMatch && (scanResult.rssi >= -100)) {
|
||||
@ -320,26 +324,47 @@ class BlueManage {
|
||||
|
||||
/// 判断是否包含指定的uuid
|
||||
bool _isMatch(List<String> serviceUuids,
|
||||
{DeviceType deviceType = DeviceType.blue}) {
|
||||
{DeviceType deviceType = DeviceType.blue, required bool isSingle}) {
|
||||
final List<String> prefixes =
|
||||
getDeviceType(deviceType).map((e) => e.toLowerCase()).toList();
|
||||
for (String uuid in serviceUuids) {
|
||||
final String cleanUuid = uuid.replaceAll('-', '').toLowerCase();
|
||||
final String cleanUuid = uuid.toLowerCase();
|
||||
if (cleanUuid.length == 8) {
|
||||
// 8位,判断前两位
|
||||
// 8位,判断第4、5位
|
||||
String pairStatus = cleanUuid.substring(4, 6); // 第4、5位(索引3和4)
|
||||
for (final prefix in prefixes) {
|
||||
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位
|
||||
final String first8 = cleanUuid.substring(0, 8);
|
||||
if (first8.length >= 4) {
|
||||
final String thirdAndFourth = first8.substring(2, 4); // 索引2和3
|
||||
if (cleanUuid.length >= 32) {
|
||||
final String thirdAndFourth = cleanUuid.substring(2, 4); // 索引2和3
|
||||
for (final prefix in prefixes) {
|
||||
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 {
|
||||
connectStateCallBack(BluetoothConnectionState.disconnected);
|
||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||
if (!F.isSKY) {
|
||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||
}
|
||||
|
||||
scanDevices.clear();
|
||||
|
||||
BuglyTool.uploadException(
|
||||
@ -598,7 +626,9 @@ class BlueManage {
|
||||
});
|
||||
} else {
|
||||
connectStateCallBack(BluetoothConnectionState.disconnected);
|
||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||
if (!F.isSKY) {
|
||||
EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds);
|
||||
}
|
||||
scanDevices.clear();
|
||||
|
||||
BuglyTool.uploadException(
|
||||
@ -791,7 +821,7 @@ class BlueManage {
|
||||
}
|
||||
}
|
||||
|
||||
// 写入
|
||||
/// 写入蓝牙特征值,并等待响应
|
||||
Future<void> writeCharacteristicWithResponse(List<int> value) async {
|
||||
final List<BluetoothService> services =
|
||||
await bluetoothConnectDevice!.discoverServices();
|
||||
@ -801,30 +831,70 @@ class BlueManage {
|
||||
in service.characteristics) {
|
||||
if (characteristic.characteristicUuid == _characteristicIdWrite) {
|
||||
try {
|
||||
// 添加重试机制
|
||||
int retryCount = 0;
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 500;
|
||||
|
||||
final List<int> valueList = value;
|
||||
final List subData = splitList(valueList, _mtuSize!);
|
||||
// AppLog.log('writeCharacteristicWithResponse _mtuSize:$_mtuSize 得到的分割数据:$subData');
|
||||
|
||||
for (int i = 0; i < subData.length; i++) {
|
||||
if (characteristic.properties.writeWithoutResponse) {
|
||||
// 使用WRITE_NO_RESPONSE属性写入值
|
||||
await characteristic.write(subData[i], withoutResponse: true);
|
||||
} else if (characteristic.properties.write) {
|
||||
// 使用WRITE属性写入值
|
||||
await characteristic.write(subData[i]);
|
||||
} else {
|
||||
// 特性不支持写入
|
||||
// 对每个数据包都应用重试逻辑
|
||||
bool packetSent = false;
|
||||
retryCount = 0;
|
||||
|
||||
while (!packetSent && retryCount < maxRetries) {
|
||||
try {
|
||||
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(
|
||||
'This characteristic does not support writing.');
|
||||
'蓝牙写入失败,数据包 ${i + 1}/${subData.length} 已达到最大重试次数');
|
||||
}
|
||||
}
|
||||
|
||||
return; // 所有数据包都发送成功
|
||||
} on Exception catch (e, s) {
|
||||
AppLog.log('APP写入失败: $e $s');
|
||||
AppLog.log('APP写入失败: $e $s');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到合适的特性用于写入
|
||||
throw Exception('未找到适合写入的蓝牙特性');
|
||||
}
|
||||
|
||||
// 停止扫描蓝牙设备
|
||||
|
||||
@ -61,16 +61,16 @@ class CommandReciverManager {
|
||||
final int dataSize = data.length;
|
||||
|
||||
// 验证CRC校验
|
||||
if (dataSize >= 2) {
|
||||
final int calculatedCrc =
|
||||
_calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2);
|
||||
final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1];
|
||||
|
||||
if (calculatedCrc != receivedCrc) {
|
||||
AppLog.log('CRC校验失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// if (dataSize >= 2) {
|
||||
// final int calculatedCrc =
|
||||
// _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2);
|
||||
// final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1];
|
||||
//
|
||||
// if (calculatedCrc != receivedCrc) {
|
||||
// throw Exception('CRC校验失败');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// 当小于包头加起来13个字节
|
||||
if (dataSize < 13) {
|
||||
return;
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/flavors.dart';
|
||||
import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart';
|
||||
|
||||
import '../../../../appRouters.dart';
|
||||
@ -179,13 +180,22 @@ class _CardDetailPageState extends State<CardDetailPage> with RouteAware {
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget:
|
||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
||||
Visibility(
|
||||
visible: !F.isSKY,
|
||||
child: Obx(
|
||||
() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 60.w,
|
||||
height: 50.h,
|
||||
child: _isAdmin(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 10.h),
|
||||
CommonItem(
|
||||
leftTitel: '操作记录'.tr,
|
||||
|
||||
@ -249,8 +249,9 @@ class CardListLogic extends BaseGetXController {
|
||||
_initReplySubscription();
|
||||
|
||||
// _initRefreshAction();
|
||||
await getICCardListData(isRefresh: true);
|
||||
}
|
||||
await getICCardListData(isRefresh: true);
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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_state.dart';
|
||||
|
||||
@ -174,13 +175,22 @@ class _FaceDetailPageState extends State<FaceDetailPage> with RouteAware {
|
||||
// isHaveRightWidget: true,
|
||||
// isHaveLine: true,
|
||||
// rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget:
|
||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
||||
Visibility(
|
||||
visible: !F.isSKY,
|
||||
child: Obx(
|
||||
() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 60.w,
|
||||
height: 50.h,
|
||||
child: _isAdmin(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 10.h),
|
||||
CommonItem(
|
||||
leftTitel: '操作记录'.tr,
|
||||
|
||||
@ -443,8 +443,9 @@ class FaceListLogic extends BaseGetXController {
|
||||
// senderCheckingCardStatus();
|
||||
|
||||
// senderCheckingUserInfoCount();
|
||||
await getFaceListData(isRefresh: true);
|
||||
}
|
||||
getFaceListData(isRefresh: true);
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:get/get.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/main/lockDetail/fingerprint/addFingerprint/addFingerprint_entity.dart';
|
||||
import 'package:star_lock/tools/dateTool.dart';
|
||||
@ -141,7 +142,7 @@ class AddFingerprintLogic extends BaseGetXController {
|
||||
break;
|
||||
case 0xFE:
|
||||
case 12:
|
||||
// 管理员已满
|
||||
// 管理员已满
|
||||
state.ifAddState.value = false;
|
||||
showToast('管理员已满'.tr, something: () {
|
||||
Get.back();
|
||||
@ -250,6 +251,12 @@ class AddFingerprintLogic extends BaseGetXController {
|
||||
|
||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||
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(
|
||||
keyID: '1',
|
||||
@ -267,8 +274,8 @@ class AddFingerprintLogic extends BaseGetXController {
|
||||
// 周循环
|
||||
startDate: int.parse(state.startDate.value) ~/ 1000,
|
||||
endDate: int.parse(state.endDate.value) ~/ 1000,
|
||||
startTime: DateTool().dateToHNString(state.effectiveDateTime.value),
|
||||
endTime: DateTool().dateToHNString(state.failureDateTime.value),
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
needAuthor: 1,
|
||||
signKey: signKeyDataList,
|
||||
privateKey: getPrivateKeyList,
|
||||
@ -324,8 +331,8 @@ class AddFingerprintLogic extends BaseGetXController {
|
||||
// 周循环
|
||||
startDate: int.parse(state.startDate.value) ~/ 1000,
|
||||
endDate: int.parse(state.endDate.value) ~/ 1000,
|
||||
startTime: DateTool().dateToHNString(state.effectiveDateTime.value),
|
||||
endTime: DateTool().dateToHNString(state.failureDateTime.value),
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
needAuthor: 1,
|
||||
signKey: signKeyDataList,
|
||||
privateKey: getPrivateKeyList,
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/flavors.dart';
|
||||
import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart';
|
||||
|
||||
import '../../../../appRouters.dart';
|
||||
@ -178,13 +179,22 @@ class _FingerprintDetailPageState extends State<FingerprintDetailPage>
|
||||
isHaveLine: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 60.w, height: 50.h, child: _isStressFingerprint()))),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget:
|
||||
SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))),
|
||||
Visibility(
|
||||
visible: !F.isSKY,
|
||||
child: Obx(
|
||||
() => CommonItem(
|
||||
leftTitel: '是否为管理员'.tr,
|
||||
rightTitle: '',
|
||||
isTipsImg: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 60.w,
|
||||
height: 50.h,
|
||||
child: _isAdmin(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(height: 10.h),
|
||||
CommonItem(
|
||||
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/lockMian/entity/lockListInfo_entity.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/tools/bugly/bugly_tool.dart';
|
||||
import 'package:star_lock/tools/throttler.dart';
|
||||
@ -572,7 +574,7 @@ class LockDetailLogic extends BaseGetXController {
|
||||
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
||||
Future<void> getLockNetToken() async {
|
||||
final LockNetTokenEntity entity = await ApiRepository.to
|
||||
.getLockNetToken(lockId: state.keyInfos.value.lockId.toString());
|
||||
.getLockNetToken(lockId: state.keyInfos.value.lockId!);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
state.lockNetToken = entity.data!.token!.toString();
|
||||
// AppLog.log('从服务器获取联网token:${state.lockNetToken}');
|
||||
@ -752,42 +754,40 @@ class LockDetailLogic extends BaseGetXController {
|
||||
eventBus.fire(RefreshLockDetailInfoDataEvent());
|
||||
}
|
||||
|
||||
/// 请求设备网络信息并设置
|
||||
void requestDeviceNetworkInfo() async {
|
||||
final DeviceNetwork deviceNetworkInfo =
|
||||
await ApiRepository.to.getDeviceNetwork(
|
||||
deviceType: 2,
|
||||
deviceMac: state.keyInfos.value.mac!,
|
||||
);
|
||||
if (deviceNetworkInfo.data?.wifiName == null ||
|
||||
deviceNetworkInfo.data?.wifiName == '') {
|
||||
return;
|
||||
} else {
|
||||
final peerId = deviceNetworkInfo?.data?.peerId;
|
||||
if (peerId == null || peerId.isEmpty || peerId == '') {
|
||||
throw Exception('设备peerId为空');
|
||||
}
|
||||
// 设置锁的peerID
|
||||
StartChartManage().lockNetworkInfo =
|
||||
deviceNetworkInfo.data ?? DeviceNetworkInfo();
|
||||
StartChartManage().lockPeerId = peerId;
|
||||
}
|
||||
}
|
||||
// /// 请求设备网络信息并设置
|
||||
// void requestDeviceNetworkInfo() async {
|
||||
// final DeviceNetwork deviceNetworkInfo =
|
||||
// await ApiRepository.to.getDeviceNetwork(
|
||||
// deviceType: 2,
|
||||
// deviceMac: state.keyInfos.value.mac!,
|
||||
// );
|
||||
// if (deviceNetworkInfo.data?.peerId == null ||
|
||||
// deviceNetworkInfo.data?.peerId == '') {
|
||||
// return;
|
||||
// }
|
||||
// final peerId = deviceNetworkInfo!.data!.peerId;
|
||||
// // 设置锁的peerID
|
||||
// StartChartManage().lockNetworkInfo =
|
||||
// deviceNetworkInfo.data ?? DeviceNetworkInfo();
|
||||
// StartChartManage().lockPeerId = peerId!;
|
||||
// }
|
||||
|
||||
/// 发送监控消息
|
||||
void sendMonitorMessage() {
|
||||
final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? [];
|
||||
final network = state.keyInfos.value.network;
|
||||
if (catEyeConfig.isNotEmpty &&
|
||||
catEyeConfig.length > 0 &&
|
||||
catEyeConfig[0].catEyeMode != 0) {
|
||||
if (StartChartManage().lockNetworkInfo.wifiName == null ||
|
||||
StartChartManage().lockNetworkInfo.wifiName == '') {
|
||||
if (network == null || network?.peerId == null || network?.peerId == '') {
|
||||
showToast('设备未配网'.tr);
|
||||
return;
|
||||
}
|
||||
// 重置丢包率监控
|
||||
// PacketLossStatistics().reset();
|
||||
// 发送监控id
|
||||
StartChartManage().startCallRequestMessageTimer(
|
||||
ToPeerId: StartChartManage().lockPeerId ?? '');
|
||||
StartChartManage()
|
||||
.startCallRequestMessageTimer(ToPeerId: network!.peerId ?? '');
|
||||
} else {
|
||||
showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr);
|
||||
}
|
||||
@ -799,12 +799,13 @@ class LockDetailLogic extends BaseGetXController {
|
||||
getServerDatetime();
|
||||
await PermissionDialog.request(Permission.location);
|
||||
await PermissionDialog.requestBluetooth();
|
||||
requestDeviceNetworkInfo();
|
||||
// requestDeviceNetworkInfo();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus
|
||||
.on<LockSetChangeSetRefreshLockDetailWithType>()
|
||||
.listen((LockSetChangeSetRefreshLockDetailWithType event) {
|
||||
@ -851,11 +852,11 @@ class LockDetailLogic extends BaseGetXController {
|
||||
}
|
||||
});
|
||||
|
||||
state.SuccessfulDistributionNetworkEvent = eventBus
|
||||
.on<SuccessfulDistributionNetwork>()
|
||||
.listen((SuccessfulDistributionNetwork event) {
|
||||
// 配网成功获取一下配网信息
|
||||
requestDeviceNetworkInfo();
|
||||
});
|
||||
// state.SuccessfulDistributionNetworkEvent = eventBus
|
||||
// .on<SuccessfulDistributionNetwork>()
|
||||
// .listen((SuccessfulDistributionNetwork event) {
|
||||
// // 配网成功获取一下配网信息
|
||||
// requestDeviceNetworkInfo();
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@ -88,7 +89,6 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
/// 路由订阅
|
||||
AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
state.isOpenLockNeedOnline.refresh();
|
||||
|
||||
}
|
||||
|
||||
StreamSubscription? _lockRefreshLockDetailInfoDataEvent;
|
||||
@ -1103,13 +1103,15 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
}));
|
||||
|
||||
// 密码
|
||||
showWidgetArr.add(bottomItem('images/main/icon_main_password.png', '密码'.tr,
|
||||
state.bottomBtnisEable.value, () {
|
||||
Get.toNamed(Routers.passwordKeyListPage,
|
||||
arguments: <String, LockListInfoItemEntity>{
|
||||
'keyInfo': state.keyInfos.value
|
||||
});
|
||||
}));
|
||||
if (state.keyInfos.value.lockFeature!.password == 1) {
|
||||
showWidgetArr.add(bottomItem('images/main/icon_main_password.png',
|
||||
'密码'.tr, state.bottomBtnisEable.value, () {
|
||||
Get.toNamed(Routers.passwordKeyListPage,
|
||||
arguments: <String, LockListInfoItemEntity>{
|
||||
'keyInfo': state.keyInfos.value
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// ic卡
|
||||
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(
|
||||
bottomItem('images/main/icon_catEyes.png', '监控'.tr,
|
||||
state.bottomBtnisEable.value, () async {
|
||||
@ -1467,7 +1469,7 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
state.iSOpenLock.value = true;
|
||||
state.openLockBtnState.value = 1;
|
||||
state.animationController!.forward();
|
||||
// AppLog.log('点击开锁');
|
||||
AppLog.log('点击开锁');
|
||||
if (isOpenLockNeedOnline) {
|
||||
// 不需要联网
|
||||
state.openDoorModel = 0;
|
||||
|
||||
@ -7,18 +7,18 @@ import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dar
|
||||
import '../../../blue/io_reply.dart';
|
||||
import '../../lockMian/entity/lockListInfo_entity.dart';
|
||||
|
||||
|
||||
class LockDetailState {
|
||||
Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
|
||||
final Rx<LockSetInfoData> lockSetInfoData = LockSetInfoData().obs;
|
||||
late StreamSubscription<Reply> replySubscription;
|
||||
StreamSubscription? lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
||||
StreamSubscription?
|
||||
lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
||||
StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription;
|
||||
StreamSubscription? DetailLockInfo;
|
||||
StreamSubscription? SuccessfulDistributionNetworkEvent;
|
||||
|
||||
String lockNetToken = '0';
|
||||
int differentialTime = 0;// 服务器时间与本地时间差值
|
||||
int differentialTime = 0; // 服务器时间与本地时间差值
|
||||
bool isHaveNetwork = true;
|
||||
int lockUserNo = 0;
|
||||
int senderUserId = 0;
|
||||
@ -41,7 +41,7 @@ class LockDetailState {
|
||||
RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用
|
||||
RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用
|
||||
|
||||
int openDoorModel = 0;// 离线开门0, 在线开门2 离线关门32 在线关门34
|
||||
int openDoorModel = 0; // 离线开门0, 在线开门2 离线关门32 在线关门34
|
||||
|
||||
//过渡动画控制器
|
||||
AnimationController? animationController;
|
||||
|
||||
@ -158,23 +158,23 @@ class _BasicInformationPageState extends State<BasicInformationPage> {
|
||||
allHeight: 70.h,
|
||||
isHaveLine: true),
|
||||
)),
|
||||
Obx(() => CommonItem(
|
||||
leftTitel: '位置信息'.tr,
|
||||
// rightTitle: state.lockBasicInfo.value.address ?? "-",
|
||||
allHeight: 80.h,
|
||||
isHaveLine: false,
|
||||
isHaveRightWidget: true,
|
||||
rightWidget: SizedBox(
|
||||
width: 300.w,
|
||||
child: Text(state.lockBasicInfo.value.address ?? '无'.tr,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
color: AppColors.darkGrayTextColor)),
|
||||
),
|
||||
)),
|
||||
// Obx(() => CommonItem(
|
||||
// leftTitel: '位置信息'.tr,
|
||||
// // rightTitle: state.lockBasicInfo.value.address ?? "-",
|
||||
// allHeight: 80.h,
|
||||
// isHaveLine: false,
|
||||
// isHaveRightWidget: true,
|
||||
// rightWidget: SizedBox(
|
||||
// width: 300.w,
|
||||
// child: Text(state.lockBasicInfo.value.address ?? '无'.tr,
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// textAlign: TextAlign.end,
|
||||
// style: TextStyle(
|
||||
// fontSize: 22.sp,
|
||||
// color: AppColors.darkGrayTextColor)),
|
||||
// ),
|
||||
// )),
|
||||
/* 2024-01-12 会议确定去掉“微信二维码” by DaisyWu
|
||||
CommonItem(
|
||||
leftTitel:
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
@ -17,31 +16,36 @@ import '../../../../tools/eventBusEventManage.dart';
|
||||
import '../../../../tools/storage.dart';
|
||||
import 'burglarAlarm_state.dart';
|
||||
|
||||
class BurglarAlarmLogic extends BaseGetXController{
|
||||
class BurglarAlarmLogic extends BaseGetXController {
|
||||
BurglarAlarmState state = BurglarAlarmState();
|
||||
|
||||
// 配置锁的常开模式设置 -> 防撬报警
|
||||
Future<void> _setLockSetGeneralSetting() async{
|
||||
Future<void> _setLockSetGeneralSetting() async {
|
||||
final LoginEntity entity = await ApiRepository.to.setBurglarAlarmData(
|
||||
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){
|
||||
// eventBus.fire(RefreshLockListInfoDataEvent());
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
eventBus.fire(RefreshLockListInfoDataEvent());
|
||||
|
||||
state.burglarAlarmEnable.value = state.burglarAlarmEnable.value == 1 ? 0 : 1;
|
||||
state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = state.burglarAlarmEnable.value;
|
||||
showToast('操作成功'.tr, something: (){
|
||||
eventBus.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||
state.burglarAlarmEnable.value =
|
||||
state.burglarAlarmEnable.value == 1 ? 0 : 1;
|
||||
state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch =
|
||||
state.burglarAlarmEnable.value;
|
||||
showToast('操作成功'.tr, something: () {
|
||||
eventBus
|
||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取解析后的数据
|
||||
late StreamSubscription<Reply> _replySubscription;
|
||||
|
||||
void _initReplySubscription() {
|
||||
_replySubscription = EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
||||
if(reply is SetSupportFunctionsNoParametersReply) {
|
||||
_replySubscription =
|
||||
EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
||||
if (reply is SetSupportFunctionsNoParametersReply) {
|
||||
_replySetSupportFunctionsWithParameters(reply);
|
||||
}
|
||||
|
||||
@ -71,7 +75,7 @@ class BurglarAlarmLogic extends BaseGetXController{
|
||||
// 设置自动落锁数据解析
|
||||
Future<void> _replySetSupportFunctionsWithParameters(Reply reply) async {
|
||||
final int status = reply.data[2];
|
||||
switch(status){
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
state.sureBtnState.value = 0;
|
||||
@ -91,41 +95,47 @@ class BurglarAlarmLogic extends BaseGetXController{
|
||||
|
||||
// 设置支持功能(带参数)
|
||||
Future<void> sendBurglarAlarm() async {
|
||||
if(state.sureBtnState.value == 1){
|
||||
if (state.sureBtnState.value == 1) {
|
||||
return;
|
||||
}
|
||||
state.sureBtnState.value = 1;
|
||||
|
||||
EasyLoading.show();
|
||||
showBlueConnetctToastTimer(action: (){
|
||||
showBlueConnetctToastTimer(action: () {
|
||||
dismissEasyLoading();
|
||||
state.sureBtnState.value = 0;
|
||||
});
|
||||
BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async {
|
||||
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState connectionState) async {
|
||||
if (connectionState == BluetoothConnectionState.connected) {
|
||||
final List<String>? privateKey = await Storage.getStringList(saveBluePrivateKey);
|
||||
final List<int> getPrivateKeyList = changeStringListToIntList(privateKey!);
|
||||
final List<String>? privateKey =
|
||||
await Storage.getStringList(saveBluePrivateKey);
|
||||
final List<int> getPrivateKeyList =
|
||||
changeStringListToIntList(privateKey!);
|
||||
|
||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||
|
||||
final List<String>? publicKey = await Storage.getStringList(saveBluePublicKey);
|
||||
final List<int> getPublicKeyList = changeStringListToIntList(publicKey!);
|
||||
final List<String>? publicKey =
|
||||
await Storage.getStringList(saveBluePublicKey);
|
||||
final List<int> getPublicKeyList =
|
||||
changeStringListToIntList(publicKey!);
|
||||
|
||||
IoSenderManage.setSupportFunctionsNoParametersCommand(
|
||||
keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(),
|
||||
userID: await Storage.getUid(),
|
||||
featureBit: 30,
|
||||
featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1,
|
||||
token: getTokenList,
|
||||
needAuthor: 1,
|
||||
publicKey: getPublicKeyList,
|
||||
privateKey: getPrivateKeyList);
|
||||
keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(),
|
||||
userID: await Storage.getUid(),
|
||||
featureBit: 30,
|
||||
featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1,
|
||||
token: getTokenList,
|
||||
needAuthor: 1,
|
||||
publicKey: getPublicKeyList,
|
||||
privateKey: getPrivateKeyList,
|
||||
);
|
||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||
dismissEasyLoading();
|
||||
cancelBlueConnetctToastTimer();
|
||||
state.sureBtnState.value = 0;
|
||||
if(state.ifCurrentScreen.value == true){
|
||||
if (state.ifCurrentScreen.value == true) {
|
||||
showBlueConnetctToast();
|
||||
}
|
||||
}
|
||||
@ -152,5 +162,4 @@ class BurglarAlarmLogic extends BaseGetXController{
|
||||
|
||||
_replySubscription.cancel();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,40 +37,46 @@ import 'configuringWifi_state.dart';
|
||||
|
||||
class ConfiguringWifiLogic extends BaseGetXController {
|
||||
final ConfiguringWifiState state = ConfiguringWifiState();
|
||||
final int _configurationTimeout = 60; // 配网超时时间(秒)
|
||||
|
||||
/// 获取WiFi锁服务IP和端口
|
||||
Future<void> getWifiLockServiceIpAndPort() async {
|
||||
final ConfiguringWifiEntity entity =
|
||||
await ApiRepository.to.getWifiLockServiceIpAndPort();
|
||||
if (entity.errorCode! == 0) {
|
||||
state.configuringWifiEntity.value = entity;
|
||||
try {
|
||||
final ConfiguringWifiEntity entity =
|
||||
await ApiRepository.to.getWifiLockServiceIpAndPort();
|
||||
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 wifiName,
|
||||
required String secretKey,
|
||||
required String deviceMac,
|
||||
required String networkMac,
|
||||
}) async {
|
||||
final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork(
|
||||
deviceType: 2,
|
||||
deviceMac: deviceMac,
|
||||
wifiName: wifiName,
|
||||
networkMac: networkMac,
|
||||
secretKey: secretKey,
|
||||
peerId: peerId,
|
||||
);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
// 设置锁的peerID
|
||||
StartChartManage().lockNetworkInfo = DeviceNetworkInfo(
|
||||
try {
|
||||
final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork(
|
||||
deviceType: 2,
|
||||
deviceMac: deviceMac,
|
||||
wifiName: wifiName,
|
||||
networkMac: networkMac,
|
||||
secretKey: secretKey,
|
||||
peerId: peerId,
|
||||
);
|
||||
|
||||
await _getUploadLockSet();
|
||||
return entity;
|
||||
} catch (e) {
|
||||
dismissEasyLoading();
|
||||
state.sureBtnState.value = 0;
|
||||
AppLog.log('网络配置异常:$e');
|
||||
return LoginEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,81 +90,165 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
if (reply is GatewayConfiguringWifiResultReply) {
|
||||
_replySenderConfiguringWifiResult(reply);
|
||||
}
|
||||
// wifi配网命令应答结果
|
||||
if (reply is GatewayConfiguringWifiReply) {
|
||||
_replySenderConfiguringWifiResult(reply);
|
||||
_replySenderConfiguringWifi(reply);
|
||||
}
|
||||
if (reply is GatewayGetStatusReply) {
|
||||
_replyGatewayGetStatusReply(reply);
|
||||
}
|
||||
// if (reply is GatewayGetStatusReply) {
|
||||
// _replyStatusInfo(reply);
|
||||
// }
|
||||
// 上传数据获取锁设置
|
||||
if (reply is UpdataLockSetReply) {
|
||||
_replyUpdataLockSetReply(reply);
|
||||
}
|
||||
AppLog.log('蓝牙回调处理完毕${EasyLoading.isShow}');
|
||||
});
|
||||
}
|
||||
|
||||
// WIFI配网结果
|
||||
Future<void> _replySenderConfiguringWifiResult(Reply reply) async {
|
||||
// WIFI配网操作结果处理
|
||||
Future<void> _replySenderConfiguringWifi(Reply reply) async {
|
||||
final int status = reply.data[2];
|
||||
// state.sureBtnState.value = 0;
|
||||
|
||||
// 取消loading超时定时器
|
||||
state.loadingTimer?.cancel();
|
||||
state.loadingTimer = null;
|
||||
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
await Storage.removeLockNetWorkInfoCache();
|
||||
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 ?? '');
|
||||
|
||||
AppLog.log('wifi配网命令回复结果:成功');
|
||||
break;
|
||||
default:
|
||||
//失败
|
||||
dismissEasyLoading(); // 关闭loading
|
||||
cancelBlueConnetctToastTimer();
|
||||
if (state.loadingTimer != null) {
|
||||
state.loadingTimer!.cancel();
|
||||
state.loadingTimer = null;
|
||||
}
|
||||
|
||||
showToast('配网失败'.tr);
|
||||
state.isLoading.value = false;
|
||||
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) {
|
||||
var jsonObject = json.decode(jsonString);
|
||||
return JsonEncoder.withIndent(' ').convert(jsonObject);
|
||||
@ -168,80 +258,111 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
Future<void> senderConfiguringWifiAction() async {
|
||||
AppLog.log('开始配网${EasyLoading.isShow}');
|
||||
|
||||
if (state.isLoading.isTrue) {
|
||||
if (state.sureBtnState.value == 1) {
|
||||
AppLog.log('正在配网中请勿重复点击');
|
||||
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;
|
||||
}
|
||||
|
||||
if (state.wifiPWDController.text.isEmpty) {
|
||||
showToast('请输入WiFi密码'.tr);
|
||||
return;
|
||||
}
|
||||
// if (state.sureBtnState.value == 1) {
|
||||
// return;
|
||||
// }
|
||||
// state.sureBtnState.value = 1;
|
||||
// 先设置sureBtnState状态,以禁用按钮
|
||||
state.sureBtnState.value = 1;
|
||||
|
||||
final GetGatewayConfigurationEntity entity =
|
||||
await ApiRepository.to.getGatewayConfigurationNotLoading(timeout: 60);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
state.getGatewayConfigurationStr = entity.data ?? '';
|
||||
// 显示loading,如果已经显示则不再重复显示
|
||||
if (!EasyLoading.isShow) {
|
||||
showEasyLoading();
|
||||
}
|
||||
|
||||
// 判断是否登录账户
|
||||
final loginData = await Storage.getLoginData();
|
||||
|
||||
// 获取app用户的peerId
|
||||
String appPeerId = loginData?.starchart?.starchartId ?? '';
|
||||
// 如果已有值,则追加
|
||||
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\"}";
|
||||
}
|
||||
showEasyLoading();
|
||||
showBlueConnetctToastTimer(action: () {
|
||||
dismissEasyLoading();
|
||||
state.isLoading.value = false;
|
||||
});
|
||||
// 设置蓝牙操作超时处理
|
||||
showBlueConnetctToastTimer(
|
||||
action: () {
|
||||
if (EasyLoading.isShow) {
|
||||
dismissEasyLoading();
|
||||
}
|
||||
state.sureBtnState.value = 0; // 连接超时时重置状态
|
||||
},
|
||||
outTimer: 30,
|
||||
);
|
||||
|
||||
// 发送配网指令
|
||||
BlueManage().blueSendData(
|
||||
BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState connectionState) async {
|
||||
if (connectionState == BluetoothConnectionState.connected) {
|
||||
IoSenderManage.gatewayConfiguringWifiCommand(
|
||||
ssid: state.wifiNameController.text,
|
||||
password: state.wifiPWDController.text,
|
||||
gatewayConfigurationStr: state.getGatewayConfigurationStr,
|
||||
);
|
||||
try {
|
||||
IoSenderManage.gatewayConfiguringWifiCommand(
|
||||
ssid: state.wifiNameController.text,
|
||||
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) {
|
||||
dismissEasyLoading();
|
||||
if (EasyLoading.isShow) {
|
||||
dismissEasyLoading();
|
||||
}
|
||||
cancelBlueConnetctToastTimer();
|
||||
state.isLoading.value = false;
|
||||
// state.sureBtnState.value = 0;
|
||||
state.sureBtnState.value = 0; // 蓝牙断开时重置状态
|
||||
if (state.ifCurrentScreen.value == true) {
|
||||
showBlueConnetctToast();
|
||||
}
|
||||
@ -249,7 +370,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
},
|
||||
isAddEquipment: false,
|
||||
);
|
||||
state.isLoading.value = true;
|
||||
}
|
||||
|
||||
// 获取设备状态
|
||||
@ -278,11 +398,15 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
final NetworkInfo _networkInfo = NetworkInfo();
|
||||
|
||||
Future<String> getWifiName() async {
|
||||
String ssid = '';
|
||||
ssid = (await _networkInfo.getWifiName())!;
|
||||
ssid = ssid ?? '';
|
||||
ssid = ssid.replaceAll(r'"', '');
|
||||
return ssid ?? '';
|
||||
try {
|
||||
String? ssid = await _networkInfo.getWifiName();
|
||||
ssid = ssid ?? '';
|
||||
ssid = ssid.replaceAll(r'"', '');
|
||||
return ssid;
|
||||
} catch (e) {
|
||||
AppLog.log('获取WiFi名称失败: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
///定位权限
|
||||
@ -308,17 +432,12 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
|
||||
getWifiLockServiceIpAndPort();
|
||||
_initReplySubscription();
|
||||
// getDevicesStatusAction();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_replySubscription.cancel();
|
||||
cancelBlueConnetctToastTimer(); // 确保取消蓝牙超时计时器
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -330,8 +449,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
// state.sureBtnState.value = 0;
|
||||
|
||||
final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel();
|
||||
// 网关MAC地址
|
||||
int index = 3;
|
||||
@ -372,25 +489,16 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
default:
|
||||
//失败
|
||||
dismissEasyLoading();
|
||||
showToast('配网失败'.tr);
|
||||
if (state.loadingTimer != null) {
|
||||
state.loadingTimer!.cancel();
|
||||
state.loadingTimer = null;
|
||||
}
|
||||
// showToast('获取设备状态失败'.tr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 上传数据获取设置
|
||||
Future<void> _getUploadLockSet() async {
|
||||
showEasyLoading();
|
||||
showBlueConnetctToastTimer(action: () {
|
||||
dismissEasyLoading();
|
||||
});
|
||||
|
||||
final List<String>? token = await Storage.getStringList(saveBlueToken);
|
||||
final List<int> getTokenList = changeStringListToIntList(token!);
|
||||
|
||||
// 蓝牙获取锁设置
|
||||
await _uploadLockSet(getTokenList);
|
||||
}
|
||||
|
||||
@ -398,44 +506,74 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
Future<void> _uploadLockSet(List<int> token) async {
|
||||
final List<String>? privateKey =
|
||||
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<int> signKeyDataList = changeStringListToIntList(signKey!);
|
||||
if (signKey == null || signKey.isEmpty) {
|
||||
throw Exception('Sign key is empty');
|
||||
}
|
||||
final List<int> signKeyDataList = changeStringListToIntList(signKey);
|
||||
|
||||
IoSenderManage.updataLockSetCommand(
|
||||
lockID: BlueManage().connectDeviceName,
|
||||
userID: await Storage.getUid(),
|
||||
token: token,
|
||||
needAuthor: 1,
|
||||
signKey: signKeyDataList,
|
||||
privateKey: getPrivateKeyList);
|
||||
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState connectionState) async {
|
||||
if (connectionState == BluetoothConnectionState.connected) {
|
||||
IoSenderManage.updataLockSetCommand(
|
||||
lockID: BlueManage().connectDeviceName,
|
||||
userID: await Storage.getUid(),
|
||||
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 {
|
||||
final int status = reply.data[2];
|
||||
dismissEasyLoading(); // 关闭loading
|
||||
// 保持loading状态直到整个过程完成
|
||||
cancelBlueConnetctToastTimer();
|
||||
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
await _lockDataUpload(
|
||||
uploadType: 1,
|
||||
recordType: 0,
|
||||
records: reply.data.sublist(7, reply.data.length));
|
||||
|
||||
break;
|
||||
|
||||
case 0x06:
|
||||
//无权限
|
||||
final List<int> token = reply.data.sublist(3, 7);
|
||||
final List<String> saveStrList = changeIntListToStringList(token);
|
||||
Storage.setStringList(saveBlueToken, saveStrList);
|
||||
|
||||
_uploadLockSet(token);
|
||||
//无权限,尝试重新获取token
|
||||
try {
|
||||
final List<int> token = reply.data.sublist(3, 7);
|
||||
final List<String> saveStrList = changeIntListToStringList(token);
|
||||
await Storage.setStringList(saveBlueToken, saveStrList);
|
||||
_uploadLockSet(token);
|
||||
} catch (e) {
|
||||
if (EasyLoading.isShow) {
|
||||
dismissEasyLoading(); // 错误时关闭loading
|
||||
}
|
||||
// showToast('获取设置权限失败:${e.toString()}'.tr);
|
||||
state.sureBtnState.value = 0; // 确保重置状态
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
dismissEasyLoading();
|
||||
cancelBlueConnetctToastTimer();
|
||||
if (EasyLoading.isShow) {
|
||||
dismissEasyLoading(); // 错误时关闭loading
|
||||
}
|
||||
// showToast('获取锁设置失败 (错误码: $status)'.tr);
|
||||
state.sureBtnState.value = 0; // 确保重置状态
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -451,19 +589,10 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
||||
recordType: recordType,
|
||||
records: records,
|
||||
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
|
||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||
eventBus.fire(SuccessfulDistributionNetwork());
|
||||
});
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
eventBus
|
||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,9 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
||||
final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic());
|
||||
final ConfiguringWifiState state = Get.find<ConfiguringWifiLogic>().state;
|
||||
|
||||
// 添加密码可见性控制
|
||||
final RxBool _obscureText = true.obs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -39,19 +42,36 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
||||
'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController),
|
||||
Container(
|
||||
width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor),
|
||||
configuringWifiTFWidget(
|
||||
configuringWifiPasswordTFWidget(
|
||||
'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController),
|
||||
SizedBox(
|
||||
height: 50.h,
|
||||
),
|
||||
Obx(
|
||||
() => SubmitBtn(
|
||||
btnName: '确定'.tr,
|
||||
isDisabled: state.isLoading.isFalse,
|
||||
onClick: state.isLoading.isTrue
|
||||
btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr,
|
||||
// 当sureBtnState为1时按钮不可用
|
||||
isDisabled: state.sureBtnState.value == 0,
|
||||
onClick: state.sureBtnState.value == 1
|
||||
? null
|
||||
: () {
|
||||
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();
|
||||
},
|
||||
),
|
||||
@ -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) {
|
||||
return Container(
|
||||
height: 65.h,
|
||||
@ -95,18 +130,14 @@ class _ConfiguringWifiPageState extends State<ConfiguringWifiPage>
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextField(
|
||||
//输入框一行
|
||||
maxLines: 1,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.deny('\n'),
|
||||
// LengthLimitingTextInputFormatter(30),
|
||||
],
|
||||
controller: controller,
|
||||
autofocus: false,
|
||||
textAlign: TextAlign.end,
|
||||
decoration: InputDecoration(
|
||||
//输入里面输入文字内边距设置
|
||||
// contentPadding: const EdgeInsets.only(top: 12.0, bottom: 8.0),
|
||||
hintText: tfStr,
|
||||
hintStyle: TextStyle(fontSize: 22.sp),
|
||||
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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
@ -33,5 +31,4 @@ class ConfiguringWifiState {
|
||||
String getGatewayConfigurationStr = '';
|
||||
|
||||
RxBool isLoading = false.obs;
|
||||
Timer? loadingTimer;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.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_protocol/io_getWifiList.dart';
|
||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||
@ -20,7 +21,9 @@ class WifiListLogic extends BaseGetXController {
|
||||
|
||||
// 获取解析后的数据
|
||||
late StreamSubscription<Reply> _replySubscription;
|
||||
Timer? _connectionTimer;
|
||||
|
||||
/// 初始化订阅,监听设备响应
|
||||
void _initReplySubscription() {
|
||||
_replySubscription =
|
||||
EventBusManager().eventBus!.on<Reply>().listen((Reply reply) {
|
||||
@ -31,78 +34,130 @@ class WifiListLogic extends BaseGetXController {
|
||||
if (reply is GatewayGetWifiListReply) {
|
||||
_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 {
|
||||
final int status = reply.data[2];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
showEasyLoading();
|
||||
//成功 - 不显示loading框,UI中已经有进度指示器
|
||||
cancelBlueConnetctToastTimer();
|
||||
Future.delayed(5.seconds, dismissEasyLoading);
|
||||
break;
|
||||
case 0x06:
|
||||
// 需要鉴权
|
||||
dismissEasyLoading();
|
||||
AppLog.log('需要设备鉴权,请重试'.tr);
|
||||
state.sureBtnState.value = 0;
|
||||
break;
|
||||
default:
|
||||
// 处理其他错误状态
|
||||
dismissEasyLoading();
|
||||
AppLog.log('获取WiFi列表失败,错误码:$status'.tr);
|
||||
state.sureBtnState.value = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取WiFi数据解析
|
||||
/// 获取WiFi数据解析
|
||||
Future<void> _replyGetWifiListParameters(Reply reply) async {
|
||||
final int status = reply.data[2];
|
||||
switch (status) {
|
||||
case 0x00:
|
||||
//成功
|
||||
// showEasyLoading();
|
||||
dismissEasyLoading();
|
||||
state.sureBtnState.value = 0;
|
||||
|
||||
if (reply.data[3] > 0) {
|
||||
reply.data.removeRange(0, 4);
|
||||
// 把得到的数据按33位分割成数组 然后塞进一个新的数组里面
|
||||
// 把得到的数据按33位分割成数组然后处理
|
||||
final List<List<int>> getList = splitList(reply.data, 33);
|
||||
final List<Map<String, String>> uploadList = <Map<String, String>>[];
|
||||
|
||||
for (int i = 0; i < getList.length; i++) {
|
||||
final List<int> indexList = getList[i];
|
||||
final Map<String, String> indexMap = <String, String>{};
|
||||
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();
|
||||
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;
|
||||
default:
|
||||
// 处理其他错误状态
|
||||
dismissEasyLoading();
|
||||
showToast('解析WiFi列表失败,错误码:$status'.tr);
|
||||
state.sureBtnState.value = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取wifi列表
|
||||
/// 获取WiFi列表
|
||||
Future<void> senderGetWifiListWifiAction() async {
|
||||
if (state.sureBtnState.value == 1) {
|
||||
return;
|
||||
}
|
||||
state.sureBtnState.value = 1;
|
||||
state.wifiNameDataList.clear(); // 清空之前的列表
|
||||
|
||||
showEasyLoading();
|
||||
// 不显示loading框,UI中已经有进度指示器
|
||||
showBlueConnetctToastTimer(action: () {
|
||||
dismissEasyLoading();
|
||||
state.sureBtnState.value = 0;
|
||||
});
|
||||
|
||||
BlueManage().blueSendData(BlueManage().connectDeviceName,
|
||||
(BluetoothConnectionState connectionState) async {
|
||||
if (connectionState == BluetoothConnectionState.connected) {
|
||||
IoSenderManage.gatewayGetWifiCommand(
|
||||
userID: await Storage.getUid(),
|
||||
);
|
||||
try {
|
||||
IoSenderManage.gatewayGetWifiCommand(
|
||||
userID: await Storage.getUid(),
|
||||
);
|
||||
} catch (e) {
|
||||
state.sureBtnState.value = 0;
|
||||
cancelBlueConnetctToastTimer();
|
||||
showToast('发送获取WiFi列表请求失败:${e.toString()}'.tr);
|
||||
}
|
||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||
dismissEasyLoading();
|
||||
state.sureBtnState.value = 0;
|
||||
cancelBlueConnetctToastTimer();
|
||||
if (state.ifCurrentScreen.value == true) {
|
||||
@ -113,22 +168,27 @@ class WifiListLogic extends BaseGetXController {
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
void onReady() async {
|
||||
super.onReady();
|
||||
|
||||
_initReplySubscription();
|
||||
await senderGetWifiListWifiAction();
|
||||
dismissEasyLoading();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
senderGetWifiListWifiAction();
|
||||
// 页面进入时标记为当前页面
|
||||
state.ifCurrentScreen.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
// 取消所有计时器和订阅,防止内存泄漏
|
||||
_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 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
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
@ -38,13 +64,15 @@ class _WifiListPageState extends State<WifiListPage> {
|
||||
barTitle: 'WIFI列表'.tr,
|
||||
haveBack: state.pageName.value == 'lockSet',
|
||||
actionsList: <Widget>[
|
||||
TextButton(
|
||||
child: Text(
|
||||
'刷新'.tr,
|
||||
style: TextStyle(color: Colors.white, fontSize: 24.sp),
|
||||
),
|
||||
onPressed: logic.senderGetWifiListWifiAction,
|
||||
),
|
||||
Obx(() => TextButton(
|
||||
child: Text(
|
||||
'刷新'.tr,
|
||||
style: TextStyle(color: Colors.white, fontSize: 24.sp),
|
||||
),
|
||||
onPressed: state.sureBtnState.value == 0
|
||||
? logic.senderGetWifiListWifiAction
|
||||
: null,
|
||||
)),
|
||||
],
|
||||
backgroundColor: AppColors.mainColor,
|
||||
),
|
||||
@ -52,26 +80,56 @@ class _WifiListPageState extends State<WifiListPage> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Obx(() => state.wifiNameDataList.value.isNotEmpty
|
||||
? ListView.builder(
|
||||
itemCount: state.wifiNameDataList.value.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
Map wifiNameStr = state.wifiNameDataList.value[index];
|
||||
return _messageListItem(
|
||||
wifiNameStr['wifiName'], wifiNameStr['rssi'], () {
|
||||
Get.toNamed(Routers.configuringWifiPage,
|
||||
arguments: {
|
||||
'lockSetInfoData':
|
||||
state.lockSetInfoData.value,
|
||||
'wifiName': wifiNameStr['wifiName'],
|
||||
'pageName': state.pageName.value,
|
||||
});
|
||||
});
|
||||
})
|
||||
: NoData(
|
||||
noDataHeight: 1.sh -
|
||||
ScreenUtil().statusBarHeight -
|
||||
ScreenUtil().bottomBarHeight -
|
||||
64.h)),
|
||||
? RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if (state.sureBtnState.value == 0) {
|
||||
await logic.senderGetWifiListWifiAction();
|
||||
}
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: state.wifiNameDataList.value.length,
|
||||
itemBuilder: (BuildContext c, int index) {
|
||||
Map wifiNameStr =
|
||||
state.wifiNameDataList.value[index];
|
||||
return _messageListItem(
|
||||
wifiNameStr['wifiName'], wifiNameStr['rssi'],
|
||||
() {
|
||||
Get.toNamed(Routers.configuringWifiPage,
|
||||
arguments: {
|
||||
'lockSetInfoData':
|
||||
state.lockSetInfoData.value,
|
||||
'wifiName': wifiNameStr['wifiName'],
|
||||
'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'
|
||||
? SubmitBtn(
|
||||
@ -140,24 +198,37 @@ class _WifiListPageState extends State<WifiListPage> {
|
||||
height: 79.h,
|
||||
width: 1.sw - 20.w * 2,
|
||||
child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
'$wifiName(${rssi}db)',
|
||||
wifiName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp, color: AppColors.blackColor),
|
||||
fontSize: 24.sp, color: AppColors.blackColor),
|
||||
),
|
||||
),
|
||||
// Text(
|
||||
// rssi,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// style: TextStyle(
|
||||
// fontSize: 22.sp, color: AppColors.blackColor),
|
||||
// )
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
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(
|
||||
btnName: '保存'.tr,
|
||||
onClick: () {
|
||||
if (state.weekDays.value.isEmpty) {
|
||||
logic.showToast('请选择常开日期'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.endTimeMinute.value < state.beginTimeMinute.value) {
|
||||
logic.showToast('结束时间不能小于开始时间哦'.tr);
|
||||
return;
|
||||
}
|
||||
logic.sendAutoLock();
|
||||
}),
|
||||
)),
|
||||
|
||||
@ -21,9 +21,10 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
||||
RemoteUnlockingState state = RemoteUnlockingState();
|
||||
|
||||
void remoteUnlockingOpenOrClose() async {
|
||||
final LoginEntity entity = await ApiRepository.to.remoteUnlockingOpenOrClose(
|
||||
lockId: state.lockSetInfoData.value.lockId!,
|
||||
remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1);
|
||||
final LoginEntity entity = await ApiRepository.to
|
||||
.remoteUnlockingOpenOrClose(
|
||||
lockId: state.lockSetInfoData.value.lockId!,
|
||||
remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
showToast('操作成功'.tr, something: () {
|
||||
eventBus.fire(RefreshLockListInfoDataEvent());
|
||||
@ -32,7 +33,6 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
||||
state.remoteEnable.value;
|
||||
eventBus
|
||||
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
|
||||
eventBus.fire(RefreshLockListInfoDataEvent());
|
||||
eventBus.fire(LockSetChangeSetRefreshLockDetailWithType(
|
||||
5,
|
||||
state.lockSetInfoData.value.lockSettingInfo!.remoteUnlock!
|
||||
@ -44,6 +44,7 @@ class RemoteUnlockingLogic extends BaseGetXController {
|
||||
|
||||
// 获取解析后的数据
|
||||
late StreamSubscription<Reply> _replySubscription;
|
||||
|
||||
void _initReplySubscription() {
|
||||
_replySubscription =
|
||||
EventBusManager().eventBus!.on<Reply>().listen((reply) {
|
||||
|
||||
@ -29,9 +29,9 @@ class CoerceOpenDoorLogic extends BaseGetXController {
|
||||
case 2:
|
||||
return '密码'.tr;
|
||||
case 3:
|
||||
return '指纹'.tr;
|
||||
return 'IC卡'.tr;
|
||||
case 4:
|
||||
return '卡'.tr;
|
||||
return '指纹'.tr;
|
||||
case 5:
|
||||
return '人脸'.tr;
|
||||
default:
|
||||
|
||||
@ -234,6 +234,7 @@ class PalmListLogic extends BaseGetXController {
|
||||
_initReplySubscription();
|
||||
|
||||
// _initRefreshAction();
|
||||
await getPalmListData(isRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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_state.dart';
|
||||
import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart';
|
||||
@ -133,7 +134,7 @@ class _PasswordKeyDetailPageState extends State<PasswordKeyDetailPage>
|
||||
action: () {}),
|
||||
Container(height: 10.h),
|
||||
Obx(() => Visibility(
|
||||
visible: state.itemData.value.isCustom! == 1,
|
||||
visible: state.itemData.value.isCustom! == 1 && !F.isSKY,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
CommonItem(
|
||||
|
||||
@ -361,6 +361,7 @@ class Bluetooth {
|
||||
class LockFeature {
|
||||
LockFeature({
|
||||
this.password,
|
||||
this.passwordIssue,
|
||||
this.icCard,
|
||||
this.fingerprint,
|
||||
this.fingerVein,
|
||||
@ -374,10 +375,14 @@ class LockFeature {
|
||||
this.isNoSupportedBlueBroadcast,
|
||||
this.wifiLockType,
|
||||
this.wifi,
|
||||
this.isH264,
|
||||
this.isH265,
|
||||
this.isMJpeg,
|
||||
});
|
||||
|
||||
LockFeature.fromJson(Map<String, dynamic> json) {
|
||||
password = json['password'];
|
||||
passwordIssue = json['passwordIssue'];
|
||||
icCard = json['icCard'];
|
||||
fingerprint = json['fingerprint'];
|
||||
fingerVein = json['fingerVein'];
|
||||
@ -391,9 +396,13 @@ class LockFeature {
|
||||
isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast'];
|
||||
wifiLockType = json['wifiLockType'];
|
||||
wifi = json['wifi'];
|
||||
isH264 = json['isH264'];
|
||||
isH265 = json['isH265'];
|
||||
isMJpeg = json['isMJpeg'];
|
||||
}
|
||||
|
||||
int? password;
|
||||
int? passwordIssue;
|
||||
int? icCard;
|
||||
int? fingerprint;
|
||||
int? fingerVein;
|
||||
@ -407,10 +416,14 @@ class LockFeature {
|
||||
int? isNoSupportedBlueBroadcast;
|
||||
int? wifiLockType;
|
||||
int? wifi;
|
||||
int? isH264;
|
||||
int? isH265;
|
||||
int? isMJpeg;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['password'] = password;
|
||||
data['passwordIssue'] = passwordIssue;
|
||||
data['icCard'] = icCard;
|
||||
data['fingerprint'] = fingerprint;
|
||||
data['fingerVein'] = fingerVein;
|
||||
@ -424,6 +437,9 @@ class LockFeature {
|
||||
data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast;
|
||||
data['wifiLockType'] = wifiLockType;
|
||||
data['wifi'] = wifi;
|
||||
data['isH264'] = isH264;
|
||||
data['isH265'] = isH265;
|
||||
data['isMJpeg'] = isMJpeg;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,13 +27,13 @@ class LockListLogic extends BaseGetXController {
|
||||
LockListLogic(this.entity) {}
|
||||
|
||||
LockListState state = LockListState();
|
||||
List<GroupList> _groupDataList = <GroupList>[];
|
||||
final RxList<GroupList> groupDataList = <GroupList>[].obs;
|
||||
LockListInfoGroupEntity? entity;
|
||||
final ShowTipView showTipView = ShowTipView();
|
||||
|
||||
List<GroupList> get groupDataList {
|
||||
List<GroupList> get groupDataListFiltered {
|
||||
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) {
|
||||
list.forEach((GroupList element) {
|
||||
element.lockList?.removeWhere((LockListInfoItemEntity element) =>
|
||||
@ -60,15 +60,12 @@ class LockListLogic extends BaseGetXController {
|
||||
//设置数据
|
||||
void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) {
|
||||
this.entity = entity;
|
||||
if (entity.pageNo == 1) {
|
||||
_groupDataList = <GroupList>[];
|
||||
}
|
||||
_groupDataList.addAll(entity.groupList!);
|
||||
update();
|
||||
groupDataList.value = entity.groupList!;
|
||||
}
|
||||
|
||||
// 监听蓝牙协议返回结果
|
||||
late StreamSubscription<Reply> _replySubscription;
|
||||
late StreamSubscription _setLockListInfoGroupEntity;
|
||||
|
||||
void _initReplySubscription() {
|
||||
_replySubscription =
|
||||
@ -336,17 +333,30 @@ class LockListLogic extends BaseGetXController {
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
_initReplySubscription();
|
||||
_initEventHandler();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// AppLog.log('onInit调用了 setLockListInfoGroupEntity');
|
||||
setLockListInfoGroupEntity(entity!);
|
||||
AppLog.log('[onInit] entity: \\${entity?.toString()}');
|
||||
if (entity != null) {
|
||||
setLockListInfoGroupEntity(entity!);
|
||||
}
|
||||
_initEventHandler();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<LockListLogic>(builder: (LockListLogic logic) {
|
||||
return Scaffold(
|
||||
body: ListView.separated(
|
||||
itemCount: logic.groupDataList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final GroupList itemData = logic.groupDataList[index];
|
||||
return _buildLockExpandedList(context, index, itemData);
|
||||
},
|
||||
shrinkWrap: true,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 1,
|
||||
color: AppColors.greyLineColor,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
return Obx(() => Scaffold(
|
||||
body: ListView.separated(
|
||||
itemCount: logic.groupDataListFiltered.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final GroupList itemData = logic.groupDataListFiltered[index];
|
||||
return _buildLockExpandedList(context, index, itemData, key: ValueKey(itemData.groupId));
|
||||
},
|
||||
shrinkWrap: true,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return const Divider(
|
||||
height: 1,
|
||||
color: AppColors.greyLineColor,
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
//设备多层级列表
|
||||
Widget _buildLockExpandedList(BuildContext context, int index,
|
||||
GroupList itemData) {
|
||||
GroupList itemData, {Key? key}) {
|
||||
final List<LockListInfoItemEntity> lockItemList =
|
||||
itemData.lockList ?? <LockListInfoItemEntity>[];
|
||||
return LockListGroupView(
|
||||
key: key,
|
||||
onTap: () {
|
||||
//是否选中组
|
||||
if (itemData.isChecked) {} else {}
|
||||
|
||||
@ -132,20 +132,21 @@ class LockMainLogic extends BaseGetXController {
|
||||
state.lockListInfoGroupEntity.refresh();
|
||||
// AppLog.log('entity:$entity state.lockListInfoGroupEntity.value.groupList!.length:${state.lockListInfoGroupEntity.value.groupList![0].lockList!.length}');
|
||||
//检测控制器是否存在
|
||||
if (Get.isRegistered<LockListLogic>()) {
|
||||
//设置控制器数据并刷新
|
||||
// AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity');
|
||||
Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||
} else {
|
||||
//延迟加载
|
||||
Future<dynamic>.delayed(200.milliseconds, () {
|
||||
if (Get.isRegistered<LockListLogic>()) {
|
||||
//设置控制器数据并刷新
|
||||
// AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity');
|
||||
Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
eventBus.fire(SetLockListInfoGroupEntity(lockListInfoGroupEntity: entity));
|
||||
// if (Get.isRegistered<LockListLogic>()) {
|
||||
// //设置控制器数据并刷新
|
||||
// // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity');
|
||||
// Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||
// } else {
|
||||
// //延迟加载
|
||||
// Future<dynamic>.delayed(500.milliseconds, () {
|
||||
// if (Get.isRegistered<LockListLogic>()) {
|
||||
// //设置控制器数据并刷新
|
||||
// // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity');
|
||||
// Get.find<LockListLogic>().setLockListInfoGroupEntity(entity);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
if (state.dataLength.value == 1) {
|
||||
if (Get.isRegistered<LockDetailLogic>()) {
|
||||
|
||||
@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider {
|
||||
);
|
||||
|
||||
// 获取手机联网token
|
||||
Future<Response> getLockNetToken(String lockId) => post(
|
||||
Future<Response> getLockNetToken(int lockId) => post(
|
||||
getLockNetTokenURL.toUrl,
|
||||
jsonEncode({
|
||||
'lockId': lockId,
|
||||
|
||||
@ -325,7 +325,7 @@ class ApiRepository {
|
||||
}
|
||||
|
||||
// 获取手机联网token
|
||||
Future<LockNetTokenEntity> getLockNetToken({required String lockId}) async {
|
||||
Future<LockNetTokenEntity> getLockNetToken({required int lockId}) async {
|
||||
final res = await apiProvider.getLockNetToken(lockId);
|
||||
return LockNetTokenEntity.fromJson(res.body);
|
||||
}
|
||||
|
||||
@ -16,4 +16,8 @@ class TalkConstant {
|
||||
videoType: [VideoTypeE.H264],
|
||||
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}';
|
||||
}
|
||||
|
||||
// 辅助函数:定长字符串编码
|
||||
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() {
|
||||
final bytes = <int>[];
|
||||
|
||||
@ -98,16 +110,19 @@ class ScpMessage {
|
||||
if (SpIndex != null) {
|
||||
bytes.add(SpIndex!);
|
||||
}
|
||||
|
||||
// FromPeerId (字符串,记录长度)
|
||||
// FromPeerId (字符串,长度固定为44字节)
|
||||
if (FromPeerId != null) {
|
||||
bytes.addAll(utf8.encode(FromPeerId!));
|
||||
}
|
||||
// FromPeerId (44字节定长)
|
||||
// bytes.addAll(encodeFixedLengthString(FromPeerId, 44));
|
||||
|
||||
// ToPeerId (字符串,假设长度固定为32字节)
|
||||
// ToPeerId (字符串,长度固定为44字节)
|
||||
if (ToPeerId != null) {
|
||||
bytes.addAll(utf8.encode(ToPeerId!));
|
||||
}
|
||||
// ToPeerId (44字节定长)
|
||||
// bytes.addAll(encodeFixedLengthString(ToPeerId, 44));
|
||||
|
||||
// PayloadType (2 bytes)
|
||||
if (PayloadType != null) {
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:get/get.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_handle.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);
|
||||
} else {
|
||||
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_pcm_sound/flutter_pcm_sound.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/talk_status.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/talk_accept.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';
|
||||
|
||||
@ -32,7 +35,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
|
||||
// 停止同意接听的重发
|
||||
startChartManage.stopTalkAcceptTimer();
|
||||
// 接听之后增加期望音频的接收
|
||||
_handleSendExpect();
|
||||
_handleSendExpect(lockPeerID: scpMessage.FromPeerId!);
|
||||
// 停止播放铃声
|
||||
stopRingtone();
|
||||
// 设置状态为接听成功
|
||||
@ -76,8 +79,48 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSendExpect() {
|
||||
// 修改预期数据并启动发送预期数据定时器,在收到回复时停止
|
||||
startChartManage.sendImageVideoAndG711AudioTalkExpectData();
|
||||
/// 收到同意接听回复之后增加音频的期望数据
|
||||
void _handleSendExpect({
|
||||
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/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/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_handle.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||
@ -61,9 +62,6 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
||||
int? spTotal,
|
||||
int? spIndex,
|
||||
int? messageId}) {
|
||||
// 获取统计信息
|
||||
final stats = PacketLossStatistics().getStatistics();
|
||||
_asyncLog('丢包统计: $stats');
|
||||
// _asyncLog(
|
||||
// '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength');
|
||||
if (messageType == MessageTypeConstant.RealTimeData) {
|
||||
@ -118,7 +116,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
||||
void _handleVideoH264(TalkData talkData) {
|
||||
final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame();
|
||||
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));
|
||||
processCompletePayload.forEach((element) {
|
||||
talkData.content = element;
|
||||
talkDataRepository.addTalkData(talkData);
|
||||
talkDataRepository.addTalkData(
|
||||
TalkDataModel(
|
||||
talkData: talkData,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -138,7 +140,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
||||
// // 转pcm数据
|
||||
// List<int> pcmBytes = G711().convertList(g711Data);
|
||||
// talkData.content = pcmBytes;
|
||||
talkDataRepository.addTalkData(talkData);
|
||||
talkDataRepository.addTalkData(
|
||||
TalkDataModel(
|
||||
talkData: talkData,
|
||||
),
|
||||
);
|
||||
} catch (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_easyloading/flutter_easyloading.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/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||
@ -20,7 +21,7 @@ import '../../star_chart_manage.dart';
|
||||
|
||||
class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
||||
implements ScpMessageHandler {
|
||||
final TalkViewState talkViewState = Get.put(TalkViewLogic()).state;
|
||||
// final TalkViewState talkViewState = Get.put(TalkViewLogic()).state;
|
||||
|
||||
@override
|
||||
void handleReq(ScpMessage scpMessage) {
|
||||
@ -40,7 +41,11 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
||||
startChartManage.stopTalkExpectMessageTimer();
|
||||
// 停止发送对讲请求
|
||||
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秒内没有收到通话保持则执行的操作);
|
||||
talkePingOverTimeTimerManager.start();
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:star_lock/appRouters.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/talk_status.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_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/talk_expect.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/storage.dart';
|
||||
import 'package:star_lock/translations/current_locale_tool.dart';
|
||||
@ -25,27 +23,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
||||
RxString currentLanguage =
|
||||
CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言
|
||||
|
||||
// 添加上次处理请求的时间戳
|
||||
int _lastRequestTime = 0;
|
||||
|
||||
@override
|
||||
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();
|
||||
|
||||
// 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态
|
||||
if (loginData != null &&
|
||||
(talkStatus.status != TalkStatus.passiveCallWaitingAnswer ||
|
||||
@ -56,7 +37,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
||||
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
||||
startChartManage.lockPeerId = scpMessage.FromPeerId!;
|
||||
// 处理收到接听请求后的事件
|
||||
_talkRequestEvent(talkObjectName: talkReq.callerName);
|
||||
_talkRequestEvent(
|
||||
talkObjectName: talkReq.callerName,
|
||||
lockPeerID: scpMessage.FromPeerId!,
|
||||
);
|
||||
|
||||
// 回复成功
|
||||
replySuccessMessage(scpMessage);
|
||||
@ -75,6 +59,11 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
||||
// 收到对讲请求的应答
|
||||
startChartManage.FromPeerId = scpMessage.ToPeerId!;
|
||||
startChartManage.ToPeerId = scpMessage.FromPeerId!;
|
||||
startChartManage.lockPeerId = scpMessage.FromPeerId!;
|
||||
// 处理预期数据格式
|
||||
_handleResponseSendExpect(
|
||||
lockPeerID: scpMessage.FromPeerId!,
|
||||
);
|
||||
// 发送预期数据
|
||||
startChartManage.startTalkExpectTimer();
|
||||
// 停止发送对讲请求
|
||||
@ -95,28 +84,56 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
||||
void handleRealTimeData(ScpMessage scpMessage) {}
|
||||
|
||||
// 来电事件的处理
|
||||
void _talkRequestEvent({required String talkObjectName}) {
|
||||
void _talkRequestEvent({
|
||||
required String talkObjectName,
|
||||
required String lockPeerID,
|
||||
}) async {
|
||||
// 发送预期数据、通知锁板需要获取视频数据
|
||||
_handleSendExpect();
|
||||
_handleRequestSendExpect(lockPeerID: lockPeerID);
|
||||
// 播放铃声
|
||||
//test:使用自定义铃声
|
||||
playRingtone();
|
||||
// 显示状态栏弹窗
|
||||
_showTalkRequestNotification(talkObjectName: talkObjectName);
|
||||
// _showTalkRequestNotification(talkObjectName: talkObjectName);
|
||||
// 设置为等待接听状态
|
||||
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
|
||||
.getDefaultTalkExpect()
|
||||
.videoType
|
||||
.indexOf(VideoTypeE.H264) ==
|
||||
-1) {
|
||||
.getDefaultTalkExpect()
|
||||
.videoType
|
||||
.contains(VideoTypeE.H264)) {
|
||||
Get.toNamed(
|
||||
Routers.starChartTalkView,
|
||||
Routers.h264WebView,
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(
|
||||
Routers.h264WebView,
|
||||
Routers.starChartTalkView,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -188,8 +205,97 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSendExpect() {
|
||||
// 修改预期数据并启动发送预期数据定时器,在收到回复时停止
|
||||
startChartManage.sendOnlyImageVideoTalkExpectData();
|
||||
/// app收到的对讲请求后,发送的预期数据
|
||||
void _handleRequestSendExpect({
|
||||
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: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';
|
||||
|
||||
class H264FrameHandler {
|
||||
|
||||
final void Function(List<int> frameData) onCompleteFrame;
|
||||
final void Function(TalkDataModel frameData) onCompleteFrame;
|
||||
|
||||
H264FrameHandler({required this.onCompleteFrame});
|
||||
|
||||
void handleFrame(TalkDataH264Frame frame) {
|
||||
onCompleteFrame(frame.frameData);
|
||||
void handleFrame(TalkDataH264Frame frame, TalkData talkData) {
|
||||
onCompleteFrame(
|
||||
TalkDataModel(talkData: talkData, talkDataH264Frame: frame));
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,10 @@ class PacketLossStatistics {
|
||||
// key: messageId, value: {totalPackets, receivedPackets}
|
||||
final Map<int, PacketInfo> _packetsMap = HashMap();
|
||||
|
||||
// 配置参数
|
||||
int _maxCapacity = 300; // 最大容量为300条记录
|
||||
int _timeoutMs = 30000; // 默认超时时间为30秒
|
||||
|
||||
// 统计信息
|
||||
int _totalMessages = 0; // 总消息数
|
||||
int _lostMessages = 0; // 丢包的消息数
|
||||
@ -18,10 +22,19 @@ class PacketLossStatistics {
|
||||
|
||||
// 记录分包数据
|
||||
void recordPacket(int messageId, int currentIndex, int totalPackets) {
|
||||
// 定期清理超时记录
|
||||
_cleanupExpiredPackets();
|
||||
|
||||
// 检查容量限制
|
||||
_checkCapacityLimit();
|
||||
|
||||
if (!_packetsMap.containsKey(messageId)) {
|
||||
_packetsMap[messageId] = PacketInfo(totalPackets);
|
||||
_totalMessages++;
|
||||
_totalPackets += totalPackets;
|
||||
} else {
|
||||
// 更新时间戳
|
||||
_packetsMap[messageId]!.timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
_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) {
|
||||
final info = _packetsMap[messageId]!;
|
||||
@ -62,6 +120,28 @@ class PacketLossStatistics {
|
||||
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() {
|
||||
_packetsMap.clear();
|
||||
@ -76,8 +156,10 @@ class PacketLossStatistics {
|
||||
class PacketInfo {
|
||||
final int totalPackets;
|
||||
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 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||
|
||||
class TalkDataRepository {
|
||||
TalkDataRepository._() {
|
||||
_talkDataStreamController = StreamController<TalkData>.broadcast(
|
||||
_talkDataStreamController = StreamController<TalkDataModel>.broadcast(
|
||||
onListen: () {
|
||||
_isListening = true;
|
||||
},
|
||||
@ -18,13 +19,13 @@ class TalkDataRepository {
|
||||
|
||||
static TalkDataRepository get instance => _instance;
|
||||
|
||||
late final StreamController<TalkData> _talkDataStreamController;
|
||||
late final StreamController<TalkDataModel> _talkDataStreamController;
|
||||
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) {
|
||||
_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/entity/scp_message.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/talke_data_over_time_timer_manager.dart';
|
||||
@ -55,10 +56,10 @@ class ScpMessageBaseHandle {
|
||||
|
||||
// 处理出完整帧数据后的回调
|
||||
final H264FrameHandler frameHandler =
|
||||
H264FrameHandler(onCompleteFrame: (frameData) {
|
||||
H264FrameHandler(onCompleteFrame: (TalkDataModel talkDataModel) {
|
||||
// 处理完整的帧数据
|
||||
TalkDataRepository.instance.addTalkData(
|
||||
TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData),
|
||||
talkDataModel,
|
||||
);
|
||||
});
|
||||
|
||||
@ -71,6 +72,7 @@ class ScpMessageBaseHandle {
|
||||
messageId: scpMessage.MessageId!,
|
||||
);
|
||||
}
|
||||
|
||||
// 回复失败消息
|
||||
void replyErrorMessage(ScpMessage scpMessage) {
|
||||
startChartManage.sendGenericRespErrorMessage(
|
||||
|
||||
@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum {
|
||||
static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264');
|
||||
static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE');
|
||||
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> [
|
||||
NONE_V,
|
||||
H264,
|
||||
IMAGE,
|
||||
VP8,
|
||||
H264_720P,
|
||||
];
|
||||
|
||||
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/status/star_chart_talk_status.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/storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@ -112,8 +113,12 @@ class StartChartManage {
|
||||
RbcuConfirm? rbcuConfirm;
|
||||
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
|
||||
|
||||
@ -227,20 +232,6 @@ class StartChartManage {
|
||||
|
||||
/// 设置数据接收回调
|
||||
_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) {
|
||||
_log(text: 'Failed to bind UDP socket: $error');
|
||||
});
|
||||
@ -419,17 +410,36 @@ class StartChartManage {
|
||||
/// 启动持续发送对讲请求
|
||||
void startCallRequestMessageTimer({required String ToPeerId}) async {
|
||||
// 如果已经处于等待接听状态就不发送
|
||||
if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
||||
// 如果是h264则跳转至webview
|
||||
if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) {
|
||||
Get.toNamed(
|
||||
Routers.h264WebView,
|
||||
);
|
||||
} else {
|
||||
Get.toNamed(
|
||||
Routers.starChartTalkView,
|
||||
);
|
||||
}
|
||||
// if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
||||
// // 如果是h264则跳转至webview
|
||||
// if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) {
|
||||
// Get.toNamed(
|
||||
// Routers.h264WebView,
|
||||
// );
|
||||
// } else {
|
||||
// Get.toNamed(
|
||||
// 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(
|
||||
@ -596,7 +606,7 @@ class StartChartManage {
|
||||
void startTalkRejectMessageTimer() async {
|
||||
try {
|
||||
int count = 0;
|
||||
final int maxCount = 10; // 最大执行次数为10秒
|
||||
final int maxCount = 3; // 最大执行次数为10秒
|
||||
|
||||
talkRejectTimer ??= Timer.periodic(
|
||||
Duration(seconds: _defaultIntervalTime),
|
||||
@ -622,6 +632,8 @@ class StartChartManage {
|
||||
stopCallRequestMessageTimer();
|
||||
stopSendingRbcuInfoMessages();
|
||||
stopSendingRbcuProBeMessages();
|
||||
stopTalkAcceptTimer();
|
||||
stopCallRequestMessageTimer();
|
||||
// 取消定时器
|
||||
|
||||
talkePingOverTimeTimerManager.cancel();
|
||||
@ -720,6 +732,8 @@ class StartChartManage {
|
||||
stopCallRequestMessageTimer();
|
||||
stopSendingRbcuInfoMessages();
|
||||
stopSendingRbcuProBeMessages();
|
||||
stopTalkAcceptTimer();
|
||||
stopCallRequestMessageTimer();
|
||||
// 取消定时器
|
||||
talkePingOverTimeTimerManager.cancel();
|
||||
talkDataOverTimeTimerManager.cancel();
|
||||
@ -1145,7 +1159,7 @@ class StartChartManage {
|
||||
}
|
||||
|
||||
void reSetDefaultTalkExpect() {
|
||||
_defaultTalkExpect = TalkConstant.ImageExpect;
|
||||
_defaultTalkExpect = TalkConstant.H264Expect;
|
||||
}
|
||||
|
||||
TalkExpectReq getDefaultTalkExpect() {
|
||||
@ -1163,12 +1177,27 @@ class StartChartManage {
|
||||
}
|
||||
|
||||
/// 修改预期接收到的数据
|
||||
void sendImageVideoAndG711AudioTalkExpectData() {
|
||||
final talkExpectReq = TalkConstant.ImageExpect;
|
||||
void sendOnlyH264VideoTalkExpectData() {
|
||||
final talkExpectReq = TalkExpectReq(
|
||||
videoType: [VideoTypeE.H264],
|
||||
audioType: [],
|
||||
);
|
||||
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: talkExpectReq);
|
||||
}
|
||||
|
||||
/// 修改预期接收到的数据
|
||||
void sendImageVideoAndG711AudioTalkExpectData() {
|
||||
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: TalkConstant.ImageExpect);
|
||||
}
|
||||
|
||||
/// 修改预期接收到的数据
|
||||
void sendH264VideoAndG711AudioTalkExpectData() {
|
||||
changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: TalkConstant.H264Expect);
|
||||
}
|
||||
|
||||
/// 发送远程开锁
|
||||
void sendRemoteUnLockMessage({
|
||||
required String bluetoothDeviceName,
|
||||
@ -1198,6 +1227,16 @@ class StartChartManage {
|
||||
|
||||
/// 销毁资源
|
||||
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;
|
||||
// 停止发送心跳消息
|
||||
stopHeartbeat();
|
||||
@ -1225,7 +1264,6 @@ class StartChartManage {
|
||||
await Storage.removerStarChartRegisterNodeInfo();
|
||||
// 关闭udp服务
|
||||
closeUdpSocket();
|
||||
PacketLossStatistics().reset();
|
||||
}
|
||||
|
||||
/// 重置数据
|
||||
|
||||
@ -30,14 +30,13 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
|
||||
// 处理应用程序进入后台的逻辑
|
||||
|
||||
final status = StartChartManage().talkStatus.status;
|
||||
|
||||
if (status == TalkStatus.passiveCallWaitingAnswer ||
|
||||
status == TalkStatus.proactivelyCallWaitingAnswer ||
|
||||
status == TalkStatus.answeredSuccessfully ||
|
||||
status == TalkStatus.uninitialized) {
|
||||
StartChartManage().destruction();
|
||||
Get.back();
|
||||
}
|
||||
StartChartManage().destruction();
|
||||
}
|
||||
|
||||
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: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';
|
||||
@ -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/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/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';
|
||||
|
||||
@ -37,32 +41,24 @@ class TalkViewLogic extends BaseGetXController {
|
||||
|
||||
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
|
||||
|
||||
final int minBufferSize = 2; // 最小缓冲2帧,约166ms
|
||||
final int maxBufferSize = 20; // 最大缓冲8帧,约666ms
|
||||
int bufferSize = 8; // 初始化为默认大小
|
||||
// 修改音频相关的成员变量
|
||||
final int minAudioBufferSize = 1; // 音频最小缓冲1帧
|
||||
final int maxAudioBufferSize = 3; // 音频最大缓冲3帧
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
int bufferSize = 8; // 增大缓冲区,满时才渲染
|
||||
|
||||
// 添加开始时间记录
|
||||
int _startTime = 0; // 开始播放时间戳
|
||||
int _startAudioTime = 0; // 开始播放时间戳
|
||||
bool _isFirstFrame = true; // 是否是第一帧
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||
|
||||
int _startAudioTime = 0; // 开始播放时间戳
|
||||
|
||||
// 定义音频帧缓冲和发送函数
|
||||
final List<int> _bufferedAudioFrames = <int>[];
|
||||
|
||||
final Map<String, ui.Image> _imageCache = {};
|
||||
// 添加监听状态和订阅引用
|
||||
bool _isListening = false;
|
||||
StreamSubscription? _streamSubscription;
|
||||
|
||||
// 添加一个变量用于记录上一帧的时间戳
|
||||
int _lastFrameTimestamp = 0; // 初始值为 0
|
||||
Timer? videoRenderTimer; // 视频渲染定时器
|
||||
|
||||
// 添加帧率计算相关变量
|
||||
int _frameCount = 0;
|
||||
int _lastFpsUpdateTime = 0;
|
||||
Timer? _fpsTimer;
|
||||
int _renderedFrameCount = 0;
|
||||
int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
/// 初始化音频播放器
|
||||
void _initFlutterPcmSound() {
|
||||
@ -96,18 +92,28 @@ class TalkViewLogic extends BaseGetXController {
|
||||
|
||||
// 监听音视频数据流
|
||||
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;
|
||||
|
||||
// 判断数据类型,进行分发处理
|
||||
switch (contentType) {
|
||||
case TalkData_ContentTypeE.G711:
|
||||
// // 第一帧到达时记录开始时间
|
||||
// if (_isFirstAudioFrame) {
|
||||
// _startAudioTime = currentTime;
|
||||
// _isFirstAudioFrame = false;
|
||||
// }
|
||||
if (_isFirstAudioFrame) {
|
||||
_startAudioTime = currentTime;
|
||||
_isFirstAudioFrame = false;
|
||||
}
|
||||
|
||||
// 计算音频延迟
|
||||
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||
@ -129,80 +135,16 @@ class TalkViewLogic extends BaseGetXController {
|
||||
_playAudioFrames();
|
||||
break;
|
||||
case TalkData_ContentTypeE.Image:
|
||||
// 第一帧到达时记录开始时间
|
||||
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);
|
||||
}
|
||||
// 固定长度缓冲区,最多保留bufferSize帧
|
||||
state.videoBuffer.add(talkData);
|
||||
// 先进行解码和缓存
|
||||
await _decodeAndCacheFrame(talkData);
|
||||
// 最后尝试播放
|
||||
_playVideoFrames();
|
||||
if (state.videoBuffer.length > bufferSize) {
|
||||
state.videoBuffer.removeAt(0); // 移除最旧帧
|
||||
}
|
||||
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() {
|
||||
// 如果缓冲区为空或未达到目标大小,不进行播放
|
||||
@ -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() {
|
||||
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 {
|
||||
final Permission permission = Permission.microphone;
|
||||
@ -498,6 +382,31 @@ class TalkViewLogic extends BaseGetXController {
|
||||
|
||||
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
|
||||
@ -512,10 +421,17 @@ class TalkViewLogic extends BaseGetXController {
|
||||
|
||||
stopProcessingAudio();
|
||||
// 清理图片缓存
|
||||
_imageCache.clear();
|
||||
// _imageCache.clear();
|
||||
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
||||
state.oneMinuteTime.value = 0;
|
||||
// 取消数据流监听
|
||||
_streamSubscription?.cancel();
|
||||
_isListening = false;
|
||||
|
||||
// 释放视频渲染定时器
|
||||
videoRenderTimer?.cancel();
|
||||
videoRenderTimer = null;
|
||||
|
||||
super.onClose();
|
||||
}
|
||||
@ -525,6 +441,9 @@ class TalkViewLogic extends BaseGetXController {
|
||||
stopProcessingAudio();
|
||||
// 重置期望数据
|
||||
StartChartManage().reSetDefaultTalkExpect();
|
||||
// 释放视频渲染定时器
|
||||
videoRenderTimer?.cancel();
|
||||
videoRenderTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -597,34 +516,40 @@ class TalkViewLogic extends BaseGetXController {
|
||||
|
||||
// 远程开锁
|
||||
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 lockListPeerId = StartChartManage().lockListPeerId;
|
||||
int lockId = lockDetailState.keyInfos.value.lockId ?? 0;
|
||||
|
||||
// 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId
|
||||
// 从列表中遍历出对应的peerId
|
||||
lockListPeerId.forEach((element) {
|
||||
if (element.network?.peerId == lockPeerId) {
|
||||
lockId = element.lockId ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
final LockSetInfoEntity lockSetInfoEntity =
|
||||
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 = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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/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/talkView/talk_view_logic.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 TalkViewState state = Get.find<TalkViewLogic>().state;
|
||||
late Stream<int> _latencyStream;
|
||||
final startChartManage = StartChartManage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -96,60 +98,55 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
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); // 选择较大的缩放比例
|
||||
|
||||
return state.listData.value.isEmpty
|
||||
? Image.asset(
|
||||
'images/main/monitorBg.png',
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: PopScope(
|
||||
canPop: false,
|
||||
child: RepaintBoundary(
|
||||
key: state.globalKey,
|
||||
child: SizedBox.expand(
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: Obx(
|
||||
() => state.currentImage.value != null
|
||||
? RawImage(
|
||||
image: state.currentImage.value,
|
||||
width: ScreenUtil().scaleWidth,
|
||||
height: ScreenUtil().scaleHeight,
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
)
|
||||
: Container(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// 全屏背景图片或渐变色背景
|
||||
Obx(() {
|
||||
if (state.listData.value.isEmpty) {
|
||||
return SizedBox.expand(
|
||||
child: Image.asset(
|
||||
'images/main/monitorBg.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
final int videoW = startChartManage.videoWidth;
|
||||
final int videoH = startChartManage.videoHeight;
|
||||
if (videoW == 320 && videoH == 240) {
|
||||
return SizedBox.expand(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF232526),
|
||||
Color(0xFF414345),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
// 视频窗口,分辨率判断
|
||||
Obx(() {
|
||||
if (state.listData.value.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final int videoW = startChartManage.videoWidth;
|
||||
final int videoH = startChartManage.videoHeight;
|
||||
if (videoW == 320 && videoH == 240) {
|
||||
return Positioned(
|
||||
top: 150.h,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildVideoWidget(),
|
||||
);
|
||||
} else {
|
||||
// 直接全屏显示
|
||||
return _buildVideoWidget();
|
||||
}
|
||||
}),
|
||||
Obx(() => state.listData.value.isEmpty
|
||||
? Positioned(
|
||||
bottom: 310.h,
|
||||
@ -158,35 +155,35 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
||||
))
|
||||
: Container()),
|
||||
Obx(
|
||||
() => state.listData.value.isNotEmpty &&
|
||||
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(),
|
||||
),
|
||||
Obx(() =>
|
||||
state.listData.value.isNotEmpty && 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(
|
||||
@ -456,7 +453,6 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
// if (state.talkStatus.value == TalkStatus.answeredSuccessfully &&
|
||||
// state.listData.value.length > 0) {
|
||||
// logic.udpOpenDoorAction();
|
||||
logic.remoteOpenLock();
|
||||
// }
|
||||
// if (UDPManage().remoteUnlock == 1) {
|
||||
// logic.udpOpenDoorAction();
|
||||
@ -464,6 +460,7 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
// } else {
|
||||
// logic.showToast('请在锁设置中开启远程开锁'.tr);
|
||||
// }
|
||||
logic.remoteOpenLock();
|
||||
},
|
||||
)
|
||||
]);
|
||||
@ -618,4 +615,68 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
// UdpTalkDataHandler().resetDataRates();
|
||||
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 hasAudioData = false.obs; // 是否有音频数据
|
||||
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
||||
// 添加图片状态变量
|
||||
final Rx<ui.Image?> currentImage = Rx<ui.Image?>(null);
|
||||
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/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_model.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/star_chart_manage.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;
|
||||
Timer? _mockDataTimer;
|
||||
int _startAudioTime = 0; // 开始播放时间戳
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||
// 定义音频帧缓冲和发送函数
|
||||
final List<int> _bufferedAudioFrames = <int>[];
|
||||
final Queue<List<int>> _frameBuffer = Queue<List<int>>();
|
||||
static const int FRAME_BUFFER_SIZE = 25;
|
||||
|
||||
// 添加监听状态和订阅引用
|
||||
bool _isListening = false;
|
||||
StreamSubscription? _streamSubscription;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// 初始化 WebView 控制器
|
||||
state.webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
@ -63,18 +71,40 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
},
|
||||
);
|
||||
|
||||
state.isShowLoading.value = true;
|
||||
// 加载本地 HTML
|
||||
_loadLocalHtml();
|
||||
super.onInit();
|
||||
// 创建流数据监听
|
||||
_createFramesStreamListen();
|
||||
// playLocalTestVideo();
|
||||
_startListenTalkStatus();
|
||||
state.talkStatus.value = state.startChartTalkStatus.status;
|
||||
// 初始化音频播放器
|
||||
_initFlutterPcmSound();
|
||||
|
||||
// 初始化录音控制器
|
||||
_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 {
|
||||
state.talkDataRepository.talkDataStream.listen((TalkData event) async {
|
||||
// 添加新帧到缓冲区
|
||||
_frameBuffer.add(event.content);
|
||||
// 防止重复监听
|
||||
if (_isListening) {
|
||||
AppLog.log("已经存在数据流监听,避免重复监听");
|
||||
return;
|
||||
}
|
||||
|
||||
// 当缓冲区超过最大容量时,发送最早的帧并移除
|
||||
while (_frameBuffer.length > FRAME_BUFFER_SIZE) {
|
||||
if (_frameBuffer.isNotEmpty) {
|
||||
final frame = _frameBuffer.removeFirst();
|
||||
await _sendBufferedData(frame);
|
||||
}
|
||||
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 (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文件
|
||||
Future<void> _loadLocalHtml() async {
|
||||
// 加载 HTML 文件内容
|
||||
@ -186,10 +311,10 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
if (state.isShowLoading.isFalse) {
|
||||
state.oneMinuteTime.value++;
|
||||
if (state.oneMinuteTime.value >= 60) {
|
||||
t.cancel(); // 取消定时器
|
||||
state.oneMinuteTime.value = 0;
|
||||
}
|
||||
// if (state.oneMinuteTime.value >= 60) {
|
||||
// t.cancel(); // 取消定时器
|
||||
// state.oneMinuteTime.value = 0;
|
||||
// }
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -321,7 +446,7 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
}
|
||||
|
||||
// 首先应用固定增益提升基础音量
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.8);
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
@ -409,7 +534,7 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
});
|
||||
|
||||
final LockSetInfoEntity lockSetInfoEntity =
|
||||
await ApiRepository.to.getLockSettingInfoData(
|
||||
await ApiRepository.to.getLockSettingInfoData(
|
||||
lockId: lockId.toString(),
|
||||
);
|
||||
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
|
||||
void dispose() {
|
||||
|
||||
@ -7,6 +7,7 @@ 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/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/handle/other/talk_data_repository.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
||||
@ -25,6 +26,7 @@ class _H264WebViewState extends State<H264WebView>
|
||||
with TickerProviderStateMixin {
|
||||
final H264WebViewLogic logic = Get.put(H264WebViewLogic());
|
||||
final H264WebViewState state = Get.find<H264WebViewLogic>().state;
|
||||
final startChartManage = StartChartManage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -71,8 +73,13 @@ class _H264WebViewState extends State<H264WebView>
|
||||
height: screenHeight,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: WebViewWidget(
|
||||
controller: state.webViewController,
|
||||
: SizedBox.expand(
|
||||
child: RotatedBox(
|
||||
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||
child: WebViewWidget(
|
||||
controller: state.webViewController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
Obx(
|
||||
@ -411,10 +418,13 @@ class _H264WebViewState extends State<H264WebView>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
state.animationController.dispose(); // 确保释放控制器
|
||||
super.dispose();
|
||||
state.animationController.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: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 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
@ -49,4 +50,6 @@ class H264WebViewState {
|
||||
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
||||
RxBool hasAudioData = false.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:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||
|
||||
import '../main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
||||
|
||||
@ -195,6 +196,7 @@ class RogerThatLockInfoDataEvent {
|
||||
class GetGatewayListRefreshUI {
|
||||
GetGatewayListRefreshUI();
|
||||
}
|
||||
|
||||
/// 同意隐私协议
|
||||
class AgreePrivacyAgreement {
|
||||
AgreePrivacyAgreement();
|
||||
@ -204,3 +206,10 @@ class AgreePrivacyAgreement {
|
||||
class SuccessfulDistributionNetwork {
|
||||
SuccessfulDistributionNetwork();
|
||||
}
|
||||
|
||||
/// 设置锁列表数据
|
||||
class SetLockListInfoGroupEntity {
|
||||
SetLockListInfoGroupEntity({required this.lockListInfoGroupEntity});
|
||||
|
||||
LockListInfoGroupEntity lockListInfoGroupEntity;
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
set -e
|
||||
APP_PRODUCT_NAME=$APP_PRODUCT_NAME
|
||||
BUILD_STATUS=$1
|
||||
if [[ "${CI_COMMIT_BRANCH}" == "release" ]] ; then
|
||||
if [[ "${CI_COMMIT_BRANCH}" == "release_sky" ]] ; then
|
||||
WECAHT_WEBHOOK_URL=$PRE_QYWECAHT_WEBHOOK_URL
|
||||
SKY_IOS_DOWNLOAD_URL=$PRE_SKY_IOS_DOWNLOAD_URL
|
||||
SKY_ANDROID_DOWNLOAD_URL=$PRE_SKY_ANDROID_DOWNLOAD_URL
|
||||
XHJ_IOS_DOWNLOAD_URL=$PRE_XHJ_IOS_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
|
||||
SKY_IOS_DOWNLOAD_URL=$DEV_SKY_IOS_DOWNLOAD_URL
|
||||
SKY_ANDROID_DOWNLOAD_URL=$DEV_SKY_ANDROID_DOWNLOAD_URL
|
||||
|
||||
@ -127,7 +127,10 @@ dependencies:
|
||||
sdk: flutter
|
||||
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:
|
||||
sdk: flutter
|
||||
|
||||
@ -277,6 +280,8 @@ dependencies:
|
||||
video_thumbnail: ^0.5.3
|
||||
# 角标管理
|
||||
flutter_app_badger: ^1.3.0
|
||||
# 滑块支持
|
||||
slide_to_act: ^2.0.2
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,63 +1,106 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 读取环境变量
|
||||
URL=$CI_API_V4_URL
|
||||
TOKEN=$GITLAB_ACCESS_TOKEN
|
||||
PROJECT_ID=$CI_PROJECT_ID
|
||||
next_tag=""
|
||||
newest_tag=""
|
||||
# ========================
|
||||
# tag_generator.sh
|
||||
# 用于自动识别并递增以 _sky 结尾的 tag(如 v1.5.556_sky),并生成下一个 tag。
|
||||
# 递增规则:feat: 提交递增 minor,fix: 或其他提交递增 patch。
|
||||
# ========================
|
||||
|
||||
# 读取环境变量(由 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"
|
||||
# 获取项目所有 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")
|
||||
#echo "tags_json:$tags_json\n"
|
||||
# 提取所有 tag 名称,按时间倒序排列(最新的在前)
|
||||
tags=$(echo "$tags_json" | jq -r '.[].name')
|
||||
tags_length=$(echo "$tags_json" | jq -r 'length')
|
||||
if [ "$tags_length" -lt 1 ]; then
|
||||
next_tag="v1.0.0"
|
||||
|
||||
# 只保留以 _sky 结尾的 tag(即只处理 v1.2.3_sky 这种格式的 tag)
|
||||
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
|
||||
newest_tag=$(echo "$tags" | head -n 1)
|
||||
IFS='.' read -r major minor patch <<< "$newest_tag"
|
||||
major="${major#v}"
|
||||
compare_json=""
|
||||
# 解析版本号部分(去掉 _sky 后缀和 v 前缀)
|
||||
# 例如 v1.5.556_sky -> 1.5.556
|
||||
version_part=${newest_sky_tag%_sky} # 去掉 _sky 后缀
|
||||
version_part=${version_part#v} # 去掉 v 前缀
|
||||
IFS='.' read -r major minor patch <<< "$version_part" # 拆分出主、次、修订号
|
||||
compare_json="" # 用于存储 commit 对比结果
|
||||
# 判断命令参数,决定对比范围
|
||||
if [[ "$1" == "generate_tag" ]];then
|
||||
echo "generate_tag:$newest_tag-to-master\n"
|
||||
compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master")
|
||||
# 生成 tag 时,对比最新 _sky tag 和 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
|
||||
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")
|
||||
fi
|
||||
echo "compare_json:$compare_json\n"
|
||||
new_patch=$patch
|
||||
new_minor=$minor
|
||||
new_patch=$patch # 新的 patch 号
|
||||
new_minor=$minor # 新的 minor 号
|
||||
# 遍历所有 commit,根据提交信息递增版本号
|
||||
while IFS= read -r commit_json; do
|
||||
# 使用 jq 解析每一行的 JSON 对象
|
||||
# 解析每个 commit 的 id 和 message
|
||||
commit_id=$(echo "$commit_json" | jq -r '.id')
|
||||
commit_message=$(echo "$commit_json" | jq -r '.message')
|
||||
echo "----$commit_message"
|
||||
# 如果有 feat: 类型提交,minor 递增(只递增一次)
|
||||
if [[ "$commit_message" =~ ("feat:"*) ]] && [[ $new_minor == $minor ]]; then
|
||||
((new_minor++))
|
||||
# new_patch=0
|
||||
# break
|
||||
# 如果有 fix: 类型提交,patch 递增
|
||||
elif [[ "$commit_message" =~ ("fix:"*) ]]; then
|
||||
((new_patch++))
|
||||
# 其他类型提交(非 Merge/Revert),patch 递增
|
||||
elif [[ ! "$commit_message" =~ ("Merge"* | "Revert"*) ]]; then
|
||||
((new_patch++))
|
||||
fi
|
||||
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
|
||||
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 [ "$next_tag" == "$newest_tag" ]; then
|
||||
if [ "$next_tag" == "$newest_sky_tag" ]; then
|
||||
echo "no change from master,skip to generate tag"
|
||||
exit 0
|
||||
fi
|
||||
# 配置 git 用户名(可根据需要补充邮箱等)
|
||||
git config user.name
|
||||
# 创建并推送新 tag
|
||||
git tag $next_tag
|
||||
git push -u origin $next_tag
|
||||
echo "generate tag: $next_tag"
|
||||
elif [[ "$1" == "generate_version" ]]; then
|
||||
# 如果是 generate_version 命令,仅导出新版本号到环境变量
|
||||
export NEXT_VERSION="$next_tag"
|
||||
echo "generate version: $NEXT_VERSION"
|
||||
fi
|
||||
# 无论哪种情况,都把新 tag 写入 app_new.version 文件
|
||||
# 供后续流程使用
|
||||
|
||||
echo "$next_tag" > app_new.version
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user