feat:增加新的中继协议逻辑
This commit is contained in:
parent
3d341e8d13
commit
fec9933c0a
@ -1096,10 +1096,8 @@
|
|||||||
"支持的国家": "支持的国家",
|
"支持的国家": "支持的国家",
|
||||||
"支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本",
|
"支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本",
|
||||||
"操作流程": "操作流程",
|
"操作流程": "操作流程",
|
||||||
|
|
||||||
"密码需至少包含数字/字母/字符中的2种组合": "密码需至少包含数字/字母/字符中的2种组合",
|
"密码需至少包含数字/字母/字符中的2种组合": "密码需至少包含数字/字母/字符中的2种组合",
|
||||||
|
"操作流程值": "1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了",
|
||||||
"操作流程值":"1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了",
|
|
||||||
"Google Home": "Google Home",
|
"Google Home": "Google Home",
|
||||||
"Action name": "Action name",
|
"Action name": "Action name",
|
||||||
"ScienerSmart": "ScienerSmart",
|
"ScienerSmart": "ScienerSmart",
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:star_lock/flavors.dart';
|
import 'package:star_lock/flavors.dart';
|
||||||
import 'package:star_lock/login/login/starLock_login_state.dart';
|
import 'package:star_lock/login/login/starLock_login_state.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/start_chart_manage.dart';
|
||||||
import 'package:star_lock/tools/appFirstEnterHandle.dart';
|
import 'package:star_lock/tools/appFirstEnterHandle.dart';
|
||||||
import 'package:star_lock/tools/wechat/customer_tool.dart';
|
import 'package:star_lock/tools/wechat/customer_tool.dart';
|
||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
@ -48,8 +48,8 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
haveBack: false,
|
haveBack: false,
|
||||||
backgroundColor: AppColors.mainColor,
|
backgroundColor: AppColors.mainColor,
|
||||||
actionsList: <Widget>[
|
actionsList: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: (){
|
onPressed: () {
|
||||||
WechatManageTool.getAppInfo(CustomerTool.openCustomerService);
|
WechatManageTool.getAppInfo(CustomerTool.openCustomerService);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@ -84,8 +84,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
width: 110.w, height: 110.w))),
|
width: 110.w, height: 110.w))),
|
||||||
SizedBox(height: 50.w),
|
SizedBox(height: 50.w),
|
||||||
Obx(() => CommonItem(
|
Obx(() => CommonItem(
|
||||||
leftTitel:
|
leftTitel: '你所在的国家/地区'.tr,
|
||||||
'你所在的国家/地区'.tr,
|
|
||||||
rightTitle: '',
|
rightTitle: '',
|
||||||
isHaveLine: true,
|
isHaveLine: true,
|
||||||
isPadding: false,
|
isPadding: false,
|
||||||
@ -123,7 +122,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
height: 36.w,
|
height: 36.w,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
hintText:'请输入手机号或者邮箱'.tr,
|
hintText: '请输入手机号或者邮箱'.tr,
|
||||||
// keyboardType: TextInputType.number,
|
// keyboardType: TextInputType.number,
|
||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
// FilteringTextInputFormatter.allow(RegExp('[0-9]')),
|
// FilteringTextInputFormatter.allow(RegExp('[0-9]')),
|
||||||
@ -185,8 +184,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Text(
|
child: Text('《${'用户协议'.tr}》',
|
||||||
'《${'用户协议'.tr}》',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.mainColor,
|
color: AppColors.mainColor,
|
||||||
fontSize: 20.sp)),
|
fontSize: 20.sp)),
|
||||||
@ -201,8 +199,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Text(
|
child: Text('《${'隐私政策'.tr}》',
|
||||||
'《${'隐私政策'.tr}》',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.mainColor,
|
color: AppColors.mainColor,
|
||||||
fontSize: 20.sp)),
|
fontSize: 20.sp)),
|
||||||
@ -236,6 +233,29 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null)),
|
: null)),
|
||||||
|
SubmitBtn(
|
||||||
|
btnName: '发送上线请求',
|
||||||
|
onClick: () {
|
||||||
|
// 注册星图节点信息
|
||||||
|
StartChartManage().clientRegister();
|
||||||
|
// 查询中继信息
|
||||||
|
StartChartManage().relayQuery();
|
||||||
|
// 发送上线请求
|
||||||
|
StartChartManage().onlineRelayService();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SubmitBtn(
|
||||||
|
btnName: '启动心跳包',
|
||||||
|
onClick: () {
|
||||||
|
StartChartManage().sendHeartbeatMessage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SubmitBtn(
|
||||||
|
btnName: '结束心跳包',
|
||||||
|
onClick: () {
|
||||||
|
StartChartManage().stopHeartbeat();
|
||||||
|
},
|
||||||
|
),
|
||||||
SizedBox(height: 50.w),
|
SizedBox(height: 50.w),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -246,8 +266,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
|
|||||||
height: 50.h,
|
height: 50.h,
|
||||||
// color: Colors.red,
|
// color: Colors.red,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text('${'忘记密码'.tr}?',
|
||||||
'${'忘记密码'.tr}?',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 22.sp, color: AppColors.mainColor)),
|
fontSize: 22.sp, color: AppColors.mainColor)),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart';
|
|||||||
import 'package:star_lock/mine/about/debug/debug_tool.dart';
|
import 'package:star_lock/mine/about/debug/debug_tool.dart';
|
||||||
import 'package:star_lock/network/api_provider.dart';
|
import 'package:star_lock/network/api_provider.dart';
|
||||||
import 'package:star_lock/network/api_repository.dart';
|
import 'package:star_lock/network/api_repository.dart';
|
||||||
|
import 'package:star_lock/network/start_chart_api.dart';
|
||||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
import 'package:star_lock/tools/device_info_service.dart';
|
import 'package:star_lock/tools/device_info_service.dart';
|
||||||
import 'package:star_lock/tools/platform_info_services.dart';
|
import 'package:star_lock/tools/platform_info_services.dart';
|
||||||
@ -62,6 +63,7 @@ Future<void> _initTranslation() async => TranslationLoader.loadTranslation();
|
|||||||
Future<void> _setCommonServices() async {
|
Future<void> _setCommonServices() async {
|
||||||
await Get.putAsync(() => StoreService().init());
|
await Get.putAsync(() => StoreService().init());
|
||||||
Get.put(ApiProvider());
|
Get.put(ApiProvider());
|
||||||
|
Get.put(StartChartApi());
|
||||||
Get.put(ApiRepository(Get.find<ApiProvider>()));
|
Get.put(ApiRepository(Get.find<ApiProvider>()));
|
||||||
if (F.isLite) {
|
if (F.isLite) {
|
||||||
//上架审核注释 获取设备信息
|
//上架审核注释 获取设备信息
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart';
|
|||||||
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||||||
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
|
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
|
||||||
import 'package:star_lock/main/lockMian/lockList/lockList_logic.dart';
|
import 'package:star_lock/main/lockMian/lockList/lockList_logic.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/start_chart_manage.dart';
|
||||||
import 'package:star_lock/tools/eventBusEventManage.dart';
|
import 'package:star_lock/tools/eventBusEventManage.dart';
|
||||||
import 'package:star_lock/tools/push/xs_jPhush.dart';
|
import 'package:star_lock/tools/push/xs_jPhush.dart';
|
||||||
import 'package:star_lock/tools/showTipView.dart';
|
import 'package:star_lock/tools/showTipView.dart';
|
||||||
@ -312,6 +313,7 @@ class LockMainLogic extends BaseGetXController {
|
|||||||
// AppLog.log('onReady() LockMainLogic');
|
// AppLog.log('onReady() LockMainLogic');
|
||||||
UdpHelp().openUDP();
|
UdpHelp().openUDP();
|
||||||
BlueManage();
|
BlueManage();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -282,4 +282,11 @@ abstract class Api {
|
|||||||
|
|
||||||
final String updateZoneOffsetsAndLanguagesURL =
|
final String updateZoneOffsetsAndLanguagesURL =
|
||||||
'/cloudUser/updateSettings'; //更新云用户时区偏移与语言
|
'/cloudUser/updateSettings'; //更新云用户时区偏移与语言
|
||||||
|
|
||||||
|
final String starChartRegisterNodeURL =
|
||||||
|
'/SL-A-1.0/peer/register'; // 星图--注册节点
|
||||||
|
final String relayQueryInfoURL =
|
||||||
|
'/SL-A-1.0/relay/query'; // 星图--中继查询
|
||||||
|
final String reportInformationDataURL =
|
||||||
|
'/SL-A-1.0/peer/login'; // 星图--信息上报
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,62 @@ class BaseProvider extends GetConnect with Api {
|
|||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
);
|
);
|
||||||
} else {}
|
} else {}
|
||||||
if(isShowNetworkErrorMsg ?? true){
|
if (isShowNetworkErrorMsg ?? true) {
|
||||||
|
getDataResult(res.body);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Response<T>> get<T>(
|
||||||
|
String url, {
|
||||||
|
String? contentType,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
Map<String, dynamic>? query,
|
||||||
|
Decoder<T>? decoder,
|
||||||
|
bool? isUnShowLoading = false, // 是否显示loading
|
||||||
|
bool? isUserBaseUrl = true, // 文件上传不使用baseUrl
|
||||||
|
bool? isShowErrMsg = true, // 是否显示没有网络时的提示
|
||||||
|
bool? isShowNetworkErrorMsg = true, // 是否显示网络其他报错 如403 500等
|
||||||
|
}) async {
|
||||||
|
AppLog.log('get: url:$url');
|
||||||
|
if (isUnShowLoading == false) {
|
||||||
|
EasyLoading.show();
|
||||||
|
}
|
||||||
|
if (isUserBaseUrl == false) {
|
||||||
|
httpClient.baseUrl = '';
|
||||||
|
} else {
|
||||||
|
httpClient.baseUrl = '${F.apiPrefix}/api';
|
||||||
|
}
|
||||||
|
var res = await super.get(url,
|
||||||
|
contentType: contentType,
|
||||||
|
headers: headers,
|
||||||
|
query: query,
|
||||||
|
decoder: decoder);
|
||||||
|
if (EasyLoading.isShow && !isUnShowLoading!) {
|
||||||
|
await EasyLoading.dismiss(animation: true);
|
||||||
|
}
|
||||||
|
if (res.body == null) {
|
||||||
|
if (res.statusCode == null && isShowErrMsg!) {
|
||||||
|
EasyLoading.showToast('网络访问失败,请检查网络是否正常'.tr,
|
||||||
|
duration: 2000.milliseconds);
|
||||||
|
}
|
||||||
|
var rs = {
|
||||||
|
'errorMsg': 'Network Error!',
|
||||||
|
'errorCode': -1,
|
||||||
|
'data': null,
|
||||||
|
'description': '表示成功或是。'
|
||||||
|
};
|
||||||
|
return Response(
|
||||||
|
request: res.request,
|
||||||
|
statusCode: -1,
|
||||||
|
bodyString: res.bodyString,
|
||||||
|
bodyBytes: res.bodyBytes,
|
||||||
|
body: rs as T,
|
||||||
|
statusText: res.statusText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isShowNetworkErrorMsg ?? true) {
|
||||||
getDataResult(res.body);
|
getDataResult(res.body);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
58
lib/network/start_chart_api.dart
Normal file
58
lib/network/start_chart_api.dart
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:star_lock/network/api_provider.dart';
|
||||||
|
import 'package:star_lock/network/api_provider_base.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/report_information_data.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart';
|
||||||
|
|
||||||
|
class StartChartApi extends BaseProvider {
|
||||||
|
// 星图url
|
||||||
|
final String _startChartHost = 'http://sls1-scd.star-lock.cn:8080';
|
||||||
|
|
||||||
|
static StartChartApi get to => Get.find<StartChartApi>();
|
||||||
|
|
||||||
|
// 星图--注册节点
|
||||||
|
Future<StarChartRegisterNodeEntity> starChartRegisterNode({
|
||||||
|
required String product,
|
||||||
|
required String model,
|
||||||
|
required String name,
|
||||||
|
required String unique,
|
||||||
|
}) async {
|
||||||
|
final response = await post(
|
||||||
|
_startChartHost + starChartRegisterNodeURL.toUrl,
|
||||||
|
jsonEncode(<String, dynamic>{
|
||||||
|
'product': product,
|
||||||
|
'model': model,
|
||||||
|
'name': name,
|
||||||
|
'unique': unique,
|
||||||
|
}),
|
||||||
|
isUnShowLoading: true,
|
||||||
|
isUserBaseUrl: false,
|
||||||
|
);
|
||||||
|
return StarChartRegisterNodeEntity.fromJson(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 星图--中继查询
|
||||||
|
Future<RelayInfoEntity> relayQueryInfo() async {
|
||||||
|
final response = await get(
|
||||||
|
_startChartHost + relayQueryInfoURL.toUrl,
|
||||||
|
isUnShowLoading: true,
|
||||||
|
isUserBaseUrl: false,
|
||||||
|
);
|
||||||
|
return RelayInfoEntity.fromJson(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 星图--上报信息至发现服务
|
||||||
|
Future<void> reportInformation({
|
||||||
|
required ReportInformationData reportInformationData,
|
||||||
|
}) async {
|
||||||
|
final response = await post(
|
||||||
|
_startChartHost + reportInformationDataURL.toUrl,
|
||||||
|
jsonEncode(reportInformationData.toJson()),
|
||||||
|
isUnShowLoading: true,
|
||||||
|
isUserBaseUrl: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/talk/startChart/command/message_command.dart
Normal file
69
lib/talk/startChart/command/message_command.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/constant/protocol_flag_constant.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/scp_message.dart';
|
||||||
|
|
||||||
|
class MessageCommand {
|
||||||
|
/// 客户端去中继上线命令
|
||||||
|
static List<int> goOnlineRelay() {
|
||||||
|
String serializedBytesString = ScpMessage(
|
||||||
|
ProtocolFlag: ProtocolFlagConstant.scp01,
|
||||||
|
MessageType: PayloadTypeConstant.goOnline,
|
||||||
|
MessageId: 1,
|
||||||
|
SpTotal: 0,
|
||||||
|
SpIndex: 0,
|
||||||
|
).serialize();
|
||||||
|
return _hexToBytes(serializedBytesString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回声测试消息
|
||||||
|
static List<int> echoMessage({
|
||||||
|
required String ToPeerId,
|
||||||
|
required String FromPeerId,
|
||||||
|
}) {
|
||||||
|
ScpMessage message = ScpMessage(
|
||||||
|
ProtocolFlag: ProtocolFlagConstant.scp01,
|
||||||
|
MessageType: PayloadTypeConstant.echoTest,
|
||||||
|
MessageId: 1,
|
||||||
|
SpTotal: 0,
|
||||||
|
SpIndex: 0,
|
||||||
|
FromPeerId: FromPeerId,
|
||||||
|
ToPeerId: ToPeerId,
|
||||||
|
Payload: 'hello',
|
||||||
|
PayloadCRC: 55230,
|
||||||
|
PayloadLength: 5,
|
||||||
|
PayloadType: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
String serializedBytesString = message.serialize();
|
||||||
|
return _hexToBytes(serializedBytesString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳消息
|
||||||
|
static List<int> heartbeatMessage() {
|
||||||
|
ScpMessage message = ScpMessage(
|
||||||
|
ProtocolFlag: ProtocolFlagConstant.scp01,
|
||||||
|
MessageType: PayloadTypeConstant.heartbeat,
|
||||||
|
MessageId: 1,
|
||||||
|
SpTotal: 0,
|
||||||
|
SpIndex: 0,
|
||||||
|
// FromPeerId: FromPeerId,
|
||||||
|
// ToPeerId: ToPeerId,
|
||||||
|
// Payload: 'hello',
|
||||||
|
// PayloadCRC: 55230,
|
||||||
|
// PayloadLength: 5,
|
||||||
|
// PayloadType: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
String serializedBytesString = message.serialize();
|
||||||
|
return _hexToBytes(serializedBytesString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:将16进制字符串转换为字节列表
|
||||||
|
static List<int> _hexToBytes(String hex) {
|
||||||
|
final bytes = <int>[];
|
||||||
|
for (int i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/talk/startChart/constant/ip_constant.dart
Normal file
7
lib/talk/startChart/constant/ip_constant.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class IpConstant {
|
||||||
|
// 上报时需要排除的ip
|
||||||
|
static const List<String> reportExcludeIp = ['127.0.0.1','::1%1'];
|
||||||
|
static const String udpUrl = 'udp://';
|
||||||
|
static const String tcpUrl = 'tcp://';
|
||||||
|
static const String httpsUrl = 'https://';
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
class ListenAddrTypeConstant {
|
||||||
|
static const String local = 'local';
|
||||||
|
static const String externally = 'externally';
|
||||||
|
static const String relay = 'relay';
|
||||||
|
}
|
||||||
10
lib/talk/startChart/constant/payload_type_constant.dart
Normal file
10
lib/talk/startChart/constant/payload_type_constant.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class PayloadTypeConstant {
|
||||||
|
// 上线
|
||||||
|
static const int goOnline = 100;
|
||||||
|
// 回声测试
|
||||||
|
static const int echoTest = 8;
|
||||||
|
// 心跳
|
||||||
|
static const int heartbeat = 110;
|
||||||
|
// UDP协议的SCD发现服务器查询中继信息
|
||||||
|
static const int query = 120;
|
||||||
|
}
|
||||||
3
lib/talk/startChart/constant/protocol_flag_constant.dart
Normal file
3
lib/talk/startChart/constant/protocol_flag_constant.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
class ProtocolFlagConstant {
|
||||||
|
static const String scp01 = 'SC01';
|
||||||
|
}
|
||||||
90
lib/talk/startChart/entity/relay_info_entity.dart
Normal file
90
lib/talk/startChart/entity/relay_info_entity.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
class RelayInfoEntity {
|
||||||
|
RelayInfoEntity({
|
||||||
|
this.msg,
|
||||||
|
this.time,
|
||||||
|
this.stun_server,
|
||||||
|
this.client_addr,
|
||||||
|
this.relay_list,
|
||||||
|
});
|
||||||
|
|
||||||
|
RelayInfoEntity.fromJson(dynamic json) {
|
||||||
|
msg = json['msg'];
|
||||||
|
time = json['time'];
|
||||||
|
stun_server = json['stun_server'];
|
||||||
|
client_addr = json['client_addr'];
|
||||||
|
relay_list = json['relay_list'] != null
|
||||||
|
? List<RelayData>.from(
|
||||||
|
json['relay_list'].map((x) => RelayData.fromJson(x)))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? msg;
|
||||||
|
int? time;
|
||||||
|
String? stun_server;
|
||||||
|
String? client_addr;
|
||||||
|
List<RelayData>? relay_list;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'msg': msg,
|
||||||
|
'time': time,
|
||||||
|
'stun_server': stun_server,
|
||||||
|
'client_addr': client_addr,
|
||||||
|
'relay_list': relay_list?.map((x) => x.toJson()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RelayInfoEntity{msg: $msg, time: $time, stun_server: $stun_server, client_addr: $client_addr, relay_list: $relay_list}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelayData {
|
||||||
|
RelayData({
|
||||||
|
this.peerID,
|
||||||
|
this.name,
|
||||||
|
this.listenAddr,
|
||||||
|
this.peerMax,
|
||||||
|
this.peerCurrent,
|
||||||
|
this.health,
|
||||||
|
this.latency,
|
||||||
|
});
|
||||||
|
|
||||||
|
RelayData.fromJson(dynamic json) {
|
||||||
|
peerID = json['peerID'];
|
||||||
|
name = json['name'];
|
||||||
|
listenAddr = json['listenAddr'];
|
||||||
|
peerMax = json['peerMax'];
|
||||||
|
peerCurrent = json['peerCurrent'];
|
||||||
|
health = json['health'];
|
||||||
|
latency = json['latency'];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? peerID;
|
||||||
|
int? time;
|
||||||
|
String? name;
|
||||||
|
String? listenAddr;
|
||||||
|
int? peerMax;
|
||||||
|
int? peerCurrent;
|
||||||
|
int? health;
|
||||||
|
int? latency;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'peerID': peerID,
|
||||||
|
'time': time,
|
||||||
|
'name': name,
|
||||||
|
'listenAddr': listenAddr,
|
||||||
|
'peerMax': peerMax,
|
||||||
|
'peerCurrent': peerCurrent,
|
||||||
|
'health': health,
|
||||||
|
'latency': latency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RelayData{peerID: $peerID, time: $time, name: $name, listenAddr: $listenAddr, peerMax: $peerMax, peerCurrent: $peerCurrent, health: $health, latency: $latency}';
|
||||||
|
}
|
||||||
|
}
|
||||||
107
lib/talk/startChart/entity/report_information_data.dart
Normal file
107
lib/talk/startChart/entity/report_information_data.dart
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
class ReportInformationData {
|
||||||
|
ReportInformationData({
|
||||||
|
this.id,
|
||||||
|
this.public_key,
|
||||||
|
this.listen_addr,
|
||||||
|
this.relay_service,
|
||||||
|
this.time,
|
||||||
|
this.sign,
|
||||||
|
});
|
||||||
|
|
||||||
|
ReportInformationData.fromJson(dynamic json) {
|
||||||
|
id = json['id'];
|
||||||
|
public_key = json['public_key'];
|
||||||
|
time = json['time'];
|
||||||
|
sign = json['sign'];
|
||||||
|
listen_addr = json['listen_addr'] != null
|
||||||
|
? List<ListenAddrData>.from(
|
||||||
|
json['listen_addr'].map((x) => ListenAddrData.fromJson(x)))
|
||||||
|
: null;
|
||||||
|
relay_service = json['relay_service'];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? id;
|
||||||
|
String? public_key;
|
||||||
|
String? sign;
|
||||||
|
List<ListenAddrData>? listen_addr;
|
||||||
|
RelayServiceData? relay_service;
|
||||||
|
int? time;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'public_key': public_key,
|
||||||
|
'time': time,
|
||||||
|
'sign': sign,
|
||||||
|
'listen_addr': listen_addr?.map((x) => x.toJson()).toList(),
|
||||||
|
'relay_service': relay_service,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ReportInformationData{id: $id, public_key: $public_key, sign: $sign, listen_addr: $listen_addr, relay_service: $relay_service, time: $time}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListenAddrData {
|
||||||
|
String? type;
|
||||||
|
String? address;
|
||||||
|
|
||||||
|
ListenAddrData({
|
||||||
|
this.type,
|
||||||
|
this.address,
|
||||||
|
});
|
||||||
|
|
||||||
|
ListenAddrData.fromJson(dynamic json) {
|
||||||
|
type = json['type'];
|
||||||
|
address = json['address'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'type': type,
|
||||||
|
'address': address,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ListenAddrData{type: $type, address: $address}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RelayServiceData {
|
||||||
|
String? name;
|
||||||
|
String? listen_addr;
|
||||||
|
int? peers_max;
|
||||||
|
int? peers_current;
|
||||||
|
|
||||||
|
RelayServiceData({
|
||||||
|
this.name,
|
||||||
|
this.listen_addr,
|
||||||
|
this.peers_max,
|
||||||
|
this.peers_current,
|
||||||
|
});
|
||||||
|
|
||||||
|
RelayServiceData.fromJson(dynamic json) {
|
||||||
|
name = json['name'];
|
||||||
|
listen_addr = json['listen_addr'];
|
||||||
|
peers_max = json['peers_max'];
|
||||||
|
peers_current = json['peers_current'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'listen_addr': listen_addr,
|
||||||
|
'peers_max': peers_max,
|
||||||
|
'peers_current': peers_current,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'RelayServiceData{name: $name, listen_addr: $listen_addr, peers_max: $peers_max, peers_current: $peers_current}';
|
||||||
|
}
|
||||||
|
}
|
||||||
143
lib/talk/startChart/entity/scp_message.dart
Normal file
143
lib/talk/startChart/entity/scp_message.dart
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:crc32_checksum/crc32_checksum.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
|
||||||
|
class ScpMessage {
|
||||||
|
ScpMessage({
|
||||||
|
this.ProtocolFlag,
|
||||||
|
this.MessageType,
|
||||||
|
this.MessageId,
|
||||||
|
this.SpTotal,
|
||||||
|
this.SpIndex,
|
||||||
|
this.FromPeerId,
|
||||||
|
this.ToPeerId,
|
||||||
|
this.PayloadType,
|
||||||
|
this.PayloadCRC,
|
||||||
|
this.PayloadLength,
|
||||||
|
this.Payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? ProtocolFlag;
|
||||||
|
int? MessageType;
|
||||||
|
int? MessageId;
|
||||||
|
int? SpTotal;
|
||||||
|
int? SpIndex;
|
||||||
|
String? FromPeerId;
|
||||||
|
String? ToPeerId;
|
||||||
|
int? PayloadType;
|
||||||
|
int? PayloadCRC;
|
||||||
|
int? PayloadLength;
|
||||||
|
String? Payload;
|
||||||
|
|
||||||
|
ScpMessage.fromJson(dynamic json) {
|
||||||
|
ProtocolFlag = json['ProtocolFlag'];
|
||||||
|
MessageType = json['MessageType'];
|
||||||
|
MessageId = json['MessageId'];
|
||||||
|
SpTotal = json['SpTotal'];
|
||||||
|
SpIndex = json['SpIndex'];
|
||||||
|
FromPeerId = json['FromPeerId'];
|
||||||
|
ToPeerId = json['ToPeerId'];
|
||||||
|
PayloadType = json['PayloadType'];
|
||||||
|
PayloadCRC = json['PayloadCRC'];
|
||||||
|
PayloadLength = json['PayloadLength'];
|
||||||
|
Payload = json['Payload'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'ProtocolFlag': ProtocolFlag,
|
||||||
|
'MessageType': MessageType,
|
||||||
|
'MessageId': MessageId,
|
||||||
|
'SpTotal': SpTotal,
|
||||||
|
'SpIndex': SpIndex,
|
||||||
|
'FromPeerId': FromPeerId,
|
||||||
|
'ToPeerId': ToPeerId,
|
||||||
|
'PayloadType': PayloadType,
|
||||||
|
'PayloadCRC': PayloadCRC,
|
||||||
|
'PayloadLength': PayloadLength,
|
||||||
|
'Payload': Payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String serialize() {
|
||||||
|
final bytes = <int>[];
|
||||||
|
|
||||||
|
// ProtocolFlag (4 bytes)
|
||||||
|
if (ProtocolFlag != null) {
|
||||||
|
bytes.addAll(utf8.encode(ProtocolFlag!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageType (1 byte)
|
||||||
|
if (MessageType != null) {
|
||||||
|
bytes.add(MessageType!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageId (2 bytes)
|
||||||
|
if (MessageId != null) {
|
||||||
|
final highByteMessageId = (MessageId! >> 8) & 0xFF;
|
||||||
|
final lowByteMessageId = MessageId! & 0xFF;
|
||||||
|
bytes.add(lowByteMessageId); // 交换位置
|
||||||
|
bytes.add(highByteMessageId); // 交换位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpTotal (1 byte)
|
||||||
|
if (SpTotal != null) {
|
||||||
|
bytes.add(SpTotal!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpIndex (1 byte)
|
||||||
|
if (SpIndex != null) {
|
||||||
|
bytes.add(SpIndex!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromPeerId (字符串,记录长度)
|
||||||
|
if (FromPeerId != null) {
|
||||||
|
bytes.addAll(utf8.encode(FromPeerId!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPeerId (字符串,假设长度固定为32字节)
|
||||||
|
if (ToPeerId != null) {
|
||||||
|
bytes.addAll(utf8.encode(ToPeerId!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadType (2 bytes)
|
||||||
|
if (PayloadType != null) {
|
||||||
|
final highBytePayloadType = (PayloadType! >> 8) & 0xFF;
|
||||||
|
final lowBytePayloadType = PayloadType! & 0xFF;
|
||||||
|
bytes.add(lowBytePayloadType); // 交换位置
|
||||||
|
bytes.add(highBytePayloadType); // 交换位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算 PayloadCRC (2 bytes)
|
||||||
|
if (PayloadCRC != null) {
|
||||||
|
final highBytePayloadCRC = (PayloadCRC! >> 8) & 0xFF;
|
||||||
|
final lowBytePayloadCRC = PayloadCRC! & 0xFF;
|
||||||
|
bytes.add(lowBytePayloadCRC); // 交换位置
|
||||||
|
bytes.add(highBytePayloadCRC); // 交换位置
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadLength (4 bytes)
|
||||||
|
if (PayloadLength != null) {
|
||||||
|
bytes.add(PayloadLength! & 0xFF);
|
||||||
|
bytes.add((PayloadLength! >> 8) & 0xFF);
|
||||||
|
bytes.add((PayloadLength! >> 16) & 0xFF);
|
||||||
|
bytes.add((PayloadLength! >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload (字符串,转换为字节)
|
||||||
|
if (Payload != null) {
|
||||||
|
bytes.addAll(utf8.encode(Payload!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转16进制字符串
|
||||||
|
final bytesToHexString = bytesToHex(bytes);
|
||||||
|
|
||||||
|
|
||||||
|
return bytesToHexString;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String bytesToHex(List<int> bytes) {
|
||||||
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
class StarChartRegisterNodeEntity {
|
||||||
|
StarChartRegisterNodeEntity({
|
||||||
|
this.msg,
|
||||||
|
this.peer,
|
||||||
|
});
|
||||||
|
|
||||||
|
StarChartRegisterNodeEntity.fromJson(dynamic json) {
|
||||||
|
msg = json['msg'];
|
||||||
|
peer = json['peer'] != null ? PeerData.fromJson(json['peer']) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? msg;
|
||||||
|
PeerData? peer;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{};
|
||||||
|
map['msg'] = msg;
|
||||||
|
if (peer != null) {
|
||||||
|
map['peer'] = peer!.toJson();
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'StarChartRegisterNodeEntity{msg: $msg, peer: $peer}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerData {
|
||||||
|
PeerData({
|
||||||
|
this.id,
|
||||||
|
this.publicKey,
|
||||||
|
this.privateKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
PeerData.fromJson(dynamic json) {
|
||||||
|
id = json['id'];
|
||||||
|
publicKey = json['publicKey'];
|
||||||
|
privateKey = json['privateKey'];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? id;
|
||||||
|
String? publicKey;
|
||||||
|
String? privateKey;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final map = <String, dynamic>{};
|
||||||
|
map['id'] = id;
|
||||||
|
map['publicKey'] = publicKey;
|
||||||
|
map['privateKey'] = privateKey;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PeerData{id: $id, publicKey: $publicKey, privateKey: $privateKey}';
|
||||||
|
}
|
||||||
|
}
|
||||||
379
lib/talk/startChart/start_chart_manage.dart
Normal file
379
lib/talk/startChart/start_chart_manage.dart
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:pointycastle/asymmetric/api.dart';
|
||||||
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/flavors.dart';
|
||||||
|
import 'package:star_lock/network/start_chart_api.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/command/message_command.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/constant/ip_constant.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/constant/listen_addr_type_constant.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/report_information_data.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart';
|
||||||
|
import 'package:star_lock/tools/deviceInfo_utils.dart';
|
||||||
|
import 'package:star_lock/tools/storage.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class StartChartManage {
|
||||||
|
// 私有构造函数,防止外部直接new对象
|
||||||
|
StartChartManage._internal();
|
||||||
|
|
||||||
|
// 单例对象
|
||||||
|
static final StartChartManage _instance = StartChartManage._internal();
|
||||||
|
|
||||||
|
// 工厂构造函数,返回单例对象
|
||||||
|
factory StartChartManage() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品昵称
|
||||||
|
final String _productName = F.navTitle;
|
||||||
|
|
||||||
|
RawDatagramSocket? _udpSocket;
|
||||||
|
late String remoteHost = ''; // 远程主机地址(服务器返回)
|
||||||
|
late int remotePort = 0; // 远程主机端口(服务器返回)
|
||||||
|
final int localPort = 62289; // 本地端口
|
||||||
|
|
||||||
|
int heartbeatIntervalTime = 1; // 心跳包间隔时间(s)
|
||||||
|
Timer? _heartBeatTimer; // 心跳包定时器
|
||||||
|
bool _heartBeatTimerRunning = false; // 心跳包定时任务发送状态
|
||||||
|
|
||||||
|
String ToPeerId = ''; // 对端ID
|
||||||
|
String FromPeerId = ''; // 我的ID
|
||||||
|
/// 客户端注册
|
||||||
|
Future<void> clientRegister() async {
|
||||||
|
// 从缓存中获取星图注册节点信息
|
||||||
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
||||||
|
await Storage.getStarChartRegisterNodeInfo();
|
||||||
|
if (starChartRegisterNodeInfo == null) {
|
||||||
|
_log(text: '开始注册客户端');
|
||||||
|
final StarChartRegisterNodeEntity requestStarChartRegisterNode =
|
||||||
|
await _requestStarChartRegisterNode();
|
||||||
|
_saveStarChartRegisterNodeToStorage(requestStarChartRegisterNode);
|
||||||
|
} else {
|
||||||
|
final entity = await Storage.getStarChartRegisterNodeInfo();
|
||||||
|
_log(text: '获取到星图注册节点信息:$entity');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中继查询
|
||||||
|
Future<void> relayQuery() async {
|
||||||
|
final RelayInfoEntity relayInfoEntity =
|
||||||
|
await StartChartApi.to.relayQueryInfo();
|
||||||
|
_saveRelayInfoEntityToStorage(relayInfoEntity);
|
||||||
|
|
||||||
|
if (relayInfoEntity.relay_list?.length != 0) {
|
||||||
|
final data = relayInfoEntity.relay_list?[0];
|
||||||
|
FromPeerId = data?.peerID ?? '';
|
||||||
|
final parseUdpUrl = _parseUdpUrl(data?.listenAddr ?? '');
|
||||||
|
remoteHost = parseUdpUrl['host'] ?? '';
|
||||||
|
remotePort = parseUdpUrl['port'] ?? '';
|
||||||
|
_log(text: '中继信息:${data}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeUdpSocket() {
|
||||||
|
if (_udpSocket != null) {
|
||||||
|
_udpSocket?.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在中继服务器中上线
|
||||||
|
Future<void> onlineRelayService() async {
|
||||||
|
await relayQuery();
|
||||||
|
var addressIListenFrom = InternetAddress.anyIPv4;
|
||||||
|
RawDatagramSocket.bind(addressIListenFrom, localPort)
|
||||||
|
.then((RawDatagramSocket socket) {
|
||||||
|
_udpSocket = socket;
|
||||||
|
|
||||||
|
/// 广播功能
|
||||||
|
_udpSocket!.broadcastEnabled = true;
|
||||||
|
|
||||||
|
/// 设置数据接收回调
|
||||||
|
_onReceiveData(_udpSocket!);
|
||||||
|
|
||||||
|
// 发送上线消息
|
||||||
|
//_sendOnlineMessage();
|
||||||
|
// 发送回声测试消息
|
||||||
|
//_sendEchoMessage();
|
||||||
|
|
||||||
|
// 上报信息
|
||||||
|
reportInformation();
|
||||||
|
}).catchError((error) {
|
||||||
|
_log(text: 'Failed to bind UDP socket: $error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收返回的数据
|
||||||
|
void _onReceiveData(RawDatagramSocket socket) {
|
||||||
|
socket.listen((RawSocketEvent event) {
|
||||||
|
if (event == RawSocketEvent.read) {
|
||||||
|
Datagram? dg = socket.receive();
|
||||||
|
try {
|
||||||
|
_log(text: '收到消息---> 长度:${dg?.data?.length}, 数据:${dg?.data}');
|
||||||
|
} catch (e) {
|
||||||
|
_log(text: '❌ Udp ----> $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上报信息至发现服务
|
||||||
|
Future<void> reportInformation() async {
|
||||||
|
_log(text: '上报信息至发现服务');
|
||||||
|
// 构建参数
|
||||||
|
ReportInformationData data = await _makeReportInformationData();
|
||||||
|
await StartChartApi.to.reportInformation(
|
||||||
|
reportInformationData: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送上线消息
|
||||||
|
void _sendOnlineMessage() {
|
||||||
|
// 组装上线消息
|
||||||
|
final message = MessageCommand.goOnlineRelay();
|
||||||
|
_sendMessage(message: message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送回声测试消息
|
||||||
|
void _sendEchoMessage() {
|
||||||
|
final message = MessageCommand.echoMessage(
|
||||||
|
ToPeerId: ToPeerId,
|
||||||
|
FromPeerId: FromPeerId,
|
||||||
|
);
|
||||||
|
_sendMessage(message: message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送心跳包消息
|
||||||
|
void sendHeartbeatMessage() {
|
||||||
|
if (_heartBeatTimerRunning) {
|
||||||
|
_log(text: '心跳已经开始了. 请勿重复发送心跳包消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_heartBeatTimer ??= Timer.periodic(
|
||||||
|
Duration(
|
||||||
|
seconds: heartbeatIntervalTime,
|
||||||
|
),
|
||||||
|
(Timer timer) {
|
||||||
|
final List<int> message = MessageCommand.heartbeatMessage();
|
||||||
|
_sendMessage(message: message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_heartBeatTimerRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止定时发送心跳包
|
||||||
|
void stopHeartbeat() {
|
||||||
|
_heartBeatTimer?.cancel();
|
||||||
|
_heartBeatTimer = null; // 清除定时器引用
|
||||||
|
_heartBeatTimerRunning = false;
|
||||||
|
_log(text: '发送心跳包结束');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
void _sendMessage({required List<int> message}) {
|
||||||
|
_log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】');
|
||||||
|
_udpSocket!.send(message, InternetAddress(remoteHost), remotePort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求注册节点
|
||||||
|
Future<StarChartRegisterNodeEntity> _requestStarChartRegisterNode() async {
|
||||||
|
// 获取设备信息
|
||||||
|
final Map<String, String> deviceInfo = await _getDeviceInfo();
|
||||||
|
// 发送注册节点请求
|
||||||
|
final StarChartRegisterNodeEntity response =
|
||||||
|
await StartChartApi.to.starChartRegisterNode(
|
||||||
|
product: _productName,
|
||||||
|
model: '${deviceInfo['brand']}_${deviceInfo['model']}',
|
||||||
|
name: '${deviceInfo['id']}',
|
||||||
|
unique: deviceInfo['id'] ?? Uuid().v1(),
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存星图注册节点信息至缓存
|
||||||
|
Future<void> _saveStarChartRegisterNodeToStorage(
|
||||||
|
StarChartRegisterNodeEntity starChartRegisterNodeEntity) async {
|
||||||
|
if (starChartRegisterNodeEntity != null) {
|
||||||
|
await Storage.saveStarChartRegisterNodeInfo(starChartRegisterNodeEntity);
|
||||||
|
_log(text: '注册成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存星图中继服务器信息至缓存
|
||||||
|
Future<void> _saveRelayInfoEntityToStorage(
|
||||||
|
RelayInfoEntity relayInfoEntity) async {
|
||||||
|
if (relayInfoEntity != null) {
|
||||||
|
await Storage.saveRelayInfo(relayInfoEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造上报信息数据参数
|
||||||
|
Future<ReportInformationData> _makeReportInformationData() async {
|
||||||
|
// 获取当前时间戳
|
||||||
|
int currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
// 获取公钥
|
||||||
|
final publicKey = await getPublicKey();
|
||||||
|
// 获取私钥
|
||||||
|
final privateKey = await getPrivateKey();
|
||||||
|
// 生成签名
|
||||||
|
final sign = await _generateSign(
|
||||||
|
currentTimestamp: currentTimestamp,
|
||||||
|
privateKey: privateKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取本机所有ip地址和中继返回的外网地址
|
||||||
|
final List<ListenAddrData> listenAddrDataList =
|
||||||
|
await _makeListenAddrDataList();
|
||||||
|
|
||||||
|
// 从缓存中获取中继信息
|
||||||
|
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
||||||
|
final RelayServiceData relayServiceData = RelayServiceData(
|
||||||
|
name: relayInfoEntity?.relay_list?[0].name ?? '',
|
||||||
|
listen_addr: relayInfoEntity?.relay_list?[0].listenAddr ?? '',
|
||||||
|
peers_max: relayInfoEntity?.relay_list?[0].peerMax ?? 0,
|
||||||
|
peers_current: relayInfoEntity?.relay_list?[0].peerCurrent ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
ReportInformationData data = ReportInformationData(
|
||||||
|
id: FromPeerId,
|
||||||
|
public_key: publicKey,
|
||||||
|
listen_addr: listenAddrDataList,
|
||||||
|
relay_service: relayServiceData,
|
||||||
|
time: currentTimestamp,
|
||||||
|
sign: sign,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本机所有ip地址和中继返回的外网地址
|
||||||
|
Future<List<ListenAddrData>> _makeListenAddrDataList() async {
|
||||||
|
final List<ListenAddrData> listenAddrDataList = [];
|
||||||
|
final List<String> localIp = await _getAllIpAddresses();
|
||||||
|
// 从缓存中获取中继信息
|
||||||
|
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
||||||
|
if (relayInfoEntity != null && relayInfoEntity.client_addr != null) {
|
||||||
|
listenAddrDataList.add(
|
||||||
|
ListenAddrData(
|
||||||
|
type: ListenAddrTypeConstant.relay,
|
||||||
|
address: IpConstant.udpUrl +
|
||||||
|
relayInfoEntity.client_addr! +
|
||||||
|
':' +
|
||||||
|
localPort.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
localIp.forEach((element) {
|
||||||
|
listenAddrDataList.add(
|
||||||
|
ListenAddrData(
|
||||||
|
type: ListenAddrTypeConstant.local,
|
||||||
|
address: IpConstant.udpUrl + element + ':' + localPort.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return listenAddrDataList ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取本机所有ip
|
||||||
|
Future<List<String>> _getAllIpAddresses() async {
|
||||||
|
final List<String> ipAddresses = [];
|
||||||
|
try {
|
||||||
|
final List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||||||
|
includeLoopback: true,
|
||||||
|
type: InternetAddressType.any,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final interface in interfaces) {
|
||||||
|
for (final address in interface.addresses) {
|
||||||
|
if (address.address.isNotEmpty &&
|
||||||
|
!IpConstant.reportExcludeIp.contains(address.address)) {
|
||||||
|
ipAddresses.add(address.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log(text: '❌--->获取本机IP时出现错误: $e');
|
||||||
|
}
|
||||||
|
return ipAddresses ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _log({required String text}) {
|
||||||
|
AppLog.log('$_productName=====${text}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取设备信息
|
||||||
|
Future<Map<String, String>> _getDeviceInfo() async {
|
||||||
|
final Map<String, String> deviceInfo =
|
||||||
|
await DeviceInfoUtils.getDeviceInfo();
|
||||||
|
return deviceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 UDP URL 并提取 IP 地址和端口号
|
||||||
|
Map<String, dynamic> _parseUdpUrl(String url) {
|
||||||
|
// 使用正则表达式匹配 IP 地址和端口号
|
||||||
|
final regex = RegExp(r'udp://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)')
|
||||||
|
.firstMatch(url);
|
||||||
|
|
||||||
|
if (regex != null) {
|
||||||
|
final ip = regex.group(1);
|
||||||
|
final portStr = regex.group(2);
|
||||||
|
final port = int.tryParse(portStr ?? '');
|
||||||
|
|
||||||
|
if (ip != null && port != null) {
|
||||||
|
return {'host': ip, 'port': port};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw FormatException('无法解析 URL 格式: $url');
|
||||||
|
}
|
||||||
|
|
||||||
|
String bytesToHex(List<int> bytes) {
|
||||||
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名sing
|
||||||
|
Future<String> _generateSign({
|
||||||
|
required int currentTimestamp,
|
||||||
|
required String privateKey,
|
||||||
|
}) async {
|
||||||
|
String resultSign = '';
|
||||||
|
try {
|
||||||
|
// 2. 将时间戳编码为小端字节序(Little Endian)
|
||||||
|
Uint8List signData = Uint8List(4);
|
||||||
|
signData.buffer
|
||||||
|
.asByteData()
|
||||||
|
.setUint32(0, currentTimestamp, Endian.little);
|
||||||
|
|
||||||
|
// 3. 使用 SHA-256 对 signData 进行哈希运算
|
||||||
|
final sha256Hash = sha256.convert(signData);
|
||||||
|
var parser = RSAKeyParser();
|
||||||
|
final RSAPrivateKey parsePrivateKey =
|
||||||
|
parser.parse('-----BEGIN RSA PRIVATE KEY-----\n' + privateKey)
|
||||||
|
as RSAPrivateKey;
|
||||||
|
} catch (e) {
|
||||||
|
_log(text: '❌--->生成签名时出现错误: $e');
|
||||||
|
}
|
||||||
|
return resultSign ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getPublicKey() async {
|
||||||
|
// 从缓存中获取星图注册节点信息
|
||||||
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
||||||
|
await Storage.getStarChartRegisterNodeInfo();
|
||||||
|
return starChartRegisterNodeInfo?.peer?.publicKey ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getPrivateKey() async {
|
||||||
|
// 从缓存中获取星图注册节点信息
|
||||||
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
||||||
|
await Storage.getStarChartRegisterNodeInfo();
|
||||||
|
return starChartRegisterNodeInfo?.peer?.privateKey ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -52,7 +52,7 @@ class UdpHelp {
|
|||||||
ipList: serversList,
|
ipList: serversList,
|
||||||
tokenStr: 'b989fa15f75c2ac02718b7c9bb64f80e',
|
tokenStr: 'b989fa15f75c2ac02718b7c9bb64f80e',
|
||||||
);
|
);
|
||||||
AppLog.log('发送心跳了');
|
// AppLog.log('发送心跳了');
|
||||||
} else {
|
} else {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,6 @@ class UDPManage {
|
|||||||
|
|
||||||
StreamSubscription<EventSendModel>? _streamSubscription;
|
StreamSubscription<EventSendModel>? _streamSubscription;
|
||||||
RawDatagramSocket? _udpSocket;
|
RawDatagramSocket? _udpSocket;
|
||||||
// String host = '47.106.143.213';
|
|
||||||
// int port = 8056;
|
|
||||||
// String? _mIp = '';
|
|
||||||
String host = '';
|
String host = '';
|
||||||
int port = 0;
|
int port = 0;
|
||||||
String lockId = ''; // 锁id 通过锁id来判断是哪把锁发对讲过来
|
String lockId = ''; // 锁id 通过锁id来判断是哪把锁发对讲过来
|
||||||
@ -46,7 +43,7 @@ class UDPManage {
|
|||||||
var listAddress = InternetAddress.lookup(host);
|
var listAddress = InternetAddress.lookup(host);
|
||||||
listAddress.then((list) {
|
listAddress.then((list) {
|
||||||
list.forEach((element) {
|
list.forEach((element) {
|
||||||
// AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}');
|
AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}');
|
||||||
host = element.address;
|
host = element.address;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -59,7 +56,7 @@ class UDPManage {
|
|||||||
// AppLog.log('❌ Udp ----> _port == 0');
|
// AppLog.log('❌ Udp ----> _port == 0');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// AppLog.log('Udp ----> host:$host port:$port');
|
AppLog.log('Udp ----> host:$host port:$port');
|
||||||
var addressIListenFrom = InternetAddress.anyIPv4;
|
var addressIListenFrom = InternetAddress.anyIPv4;
|
||||||
int portIListenOn = 62288;
|
int portIListenOn = 62288;
|
||||||
// if(addressIListenFrom.address != '0.0.0.0'){
|
// if(addressIListenFrom.address != '0.0.0.0'){
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class CommandUDPReciverManager {
|
|||||||
if (dataSize < 4) {
|
if (dataSize < 4) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppLog.log('appReceiveUDPData:$data');
|
// AppLog.log('appReceiveUDPData:$data');
|
||||||
|
|
||||||
final Uint8List data1 = Uint8List.fromList(data);
|
final Uint8List data1 = Uint8List.fromList(data);
|
||||||
if (data1.length == 1) {
|
if (data1.length == 1) {
|
||||||
|
|||||||
39
lib/tools/deviceInfo_utils.dart
Normal file
39
lib/tools/deviceInfo_utils.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
|
class DeviceInfoUtils {
|
||||||
|
static Future<Map<String, String>> getDeviceInfo() async {
|
||||||
|
Map<String, String> deviceInfo = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取设备信息
|
||||||
|
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
|
if (GetPlatform.isAndroid) {
|
||||||
|
AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||||
|
deviceInfo['model'] = androidInfo.model;
|
||||||
|
deviceInfo['deviceName'] = androidInfo.device;
|
||||||
|
deviceInfo['brand'] = androidInfo.brand;
|
||||||
|
deviceInfo['id'] = androidInfo.id;
|
||||||
|
// deviceInfo['uniqueIdentifier'] = androidInfo.androidId ?? 'N/A'; // 使用 androidId 作为替代
|
||||||
|
} else if (GetPlatform.isIOS) {
|
||||||
|
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||||
|
// deviceInfo['model'] = iosInfo.model;
|
||||||
|
// deviceInfo['deviceName'] = iosInfo.name;
|
||||||
|
deviceInfo['uniqueIdentifier'] =
|
||||||
|
iosInfo.identifierForVendor ?? 'N/A'; // 使用 identifierForVendor 作为替代
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 APP 版本
|
||||||
|
deviceInfo['appVersion'] = packageInfo.version;
|
||||||
|
deviceInfo['appName'] = packageInfo.appName;
|
||||||
|
} catch (e) {
|
||||||
|
print("Failed to get device info: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart';
|
||||||
|
import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart';
|
||||||
|
|
||||||
import '../login/login/entity/LoginData.dart';
|
import '../login/login/entity/LoginData.dart';
|
||||||
import '../main/lockMian/entity/lockListInfo_entity.dart';
|
import '../main/lockMian/entity/lockListInfo_entity.dart';
|
||||||
@ -29,6 +31,8 @@ const String saveLockMainListData = 'lockMainListData';
|
|||||||
const String isOpenDeBug = 'isOpenDeBug'; //是否打开 debug
|
const String isOpenDeBug = 'isOpenDeBug'; //是否打开 debug
|
||||||
const String automaticLockOffTime = 'automaticLockOffTime'; //自动关锁时间
|
const String automaticLockOffTime = 'automaticLockOffTime'; //自动关锁时间
|
||||||
const String associationUrl = 'associationUrl'; //ios跳转微信的 url
|
const String associationUrl = 'associationUrl'; //ios跳转微信的 url
|
||||||
|
const String starChartRegisterNodeInfo = 'starChartRegisterNodeInfo'; //星图注册信息
|
||||||
|
const String relayInfo = 'relayInfo'; //星图中继服务器信息
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
factory Storage() => _instance;
|
factory Storage() => _instance;
|
||||||
@ -234,4 +238,38 @@ class Storage {
|
|||||||
final String data = await Storage.getString(associationUrl) ?? '0';
|
final String data = await Storage.getString(associationUrl) ?? '0';
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取星图注册节点信息
|
||||||
|
static Future<StarChartRegisterNodeEntity?>
|
||||||
|
getStarChartRegisterNodeInfo() async {
|
||||||
|
StarChartRegisterNodeEntity? starChartRegisterNodeEntity;
|
||||||
|
final String? data = await Storage.getString(starChartRegisterNodeInfo);
|
||||||
|
if (data != null && data.isNotEmpty) {
|
||||||
|
starChartRegisterNodeEntity =
|
||||||
|
StarChartRegisterNodeEntity.fromJson(jsonDecode(data));
|
||||||
|
}
|
||||||
|
return starChartRegisterNodeEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存星图注册节点信息
|
||||||
|
static Future<void> saveStarChartRegisterNodeInfo(
|
||||||
|
StarChartRegisterNodeEntity starChartRegisterNodeEntity) async {
|
||||||
|
await Storage.setString(
|
||||||
|
starChartRegisterNodeInfo, jsonEncode(starChartRegisterNodeEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存中继服务信息
|
||||||
|
static Future<void> saveRelayInfo(RelayInfoEntity relayInfoEntity) async {
|
||||||
|
await Storage.setString(relayInfo, jsonEncode(relayInfoEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取星图注册节点信息
|
||||||
|
static Future<RelayInfoEntity?> getRelayInfo() async {
|
||||||
|
RelayInfoEntity? relayInfoEntity;
|
||||||
|
final String? data = await Storage.getString(relayInfo);
|
||||||
|
if (data != null && data.isNotEmpty) {
|
||||||
|
relayInfoEntity = RelayInfoEntity.fromJson(jsonDecode(data));
|
||||||
|
}
|
||||||
|
return relayInfoEntity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,6 +184,7 @@ dependencies:
|
|||||||
#加密解密
|
#加密解密
|
||||||
encrypt: ^5.0.1
|
encrypt: ^5.0.1
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
|
pointycastle: ^3.3.0
|
||||||
date_format: ^2.0.7
|
date_format: ^2.0.7
|
||||||
|
|
||||||
# 下拉刷新
|
# 下拉刷新
|
||||||
@ -233,7 +234,7 @@ dependencies:
|
|||||||
timelines: ^0.1.0
|
timelines: ^0.1.0
|
||||||
#侧滑删除
|
#侧滑删除
|
||||||
flutter_slidable: ^3.0.1
|
flutter_slidable: ^3.0.1
|
||||||
# audio_service: ^0.18.12
|
# audio_service: ^0.18.12
|
||||||
app_settings: ^5.1.1
|
app_settings: ^5.1.1
|
||||||
flutter_local_notifications: ^17.0.0
|
flutter_local_notifications: ^17.0.0
|
||||||
fluwx: ^4.5.5
|
fluwx: ^4.5.5
|
||||||
@ -242,10 +243,13 @@ dependencies:
|
|||||||
colorfilter_generator: ^0.0.8
|
colorfilter_generator: ^0.0.8
|
||||||
file_picker: ^5.3.1
|
file_picker: ^5.3.1
|
||||||
# 错误日志监控
|
# 错误日志监控
|
||||||
# flutter_bugly_plugin: ^0.0.9
|
# flutter_bugly_plugin: ^0.0.9
|
||||||
flutter_bugly: ^1.0.2
|
flutter_bugly: ^1.0.2
|
||||||
open_filex: ^4.4.0
|
open_filex: ^4.4.0
|
||||||
|
|
||||||
|
crc32_checksum: ^0.0.2
|
||||||
|
fast_rsa: ^3.6.6
|
||||||
|
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
#强制设置google_maps_flutter_ios 为 2.5.2
|
#强制设置google_maps_flutter_ios 为 2.5.2
|
||||||
@ -255,7 +259,6 @@ dependency_overrides:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint fset provided by the package is
|
# encourage good coding practices. The lint fset provided by the package is
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user