蓝牙和上位机和非上位机的功能的适配
This commit is contained in:
@@ -16,6 +16,9 @@ class BleManager {
|
||||
BluetoothCharacteristic? _writeCharacteristic;
|
||||
BluetoothCharacteristic? _readCharacteristic;
|
||||
|
||||
/// 🔥 已连接设备的名称(连接时从扫描结果或设备属性保存,断开时清空)
|
||||
String? _connectedDeviceName;
|
||||
|
||||
/// 有状态的协议解析器(支持 BLE 分片)
|
||||
final ProtocolParser _parser = ProtocolParser();
|
||||
|
||||
@@ -52,11 +55,19 @@ class BleManager {
|
||||
bool get isConnected => _connectedDevice != null;
|
||||
BluetoothDevice? get connectedDevice => _connectedDevice;
|
||||
BluetoothDevice? get connectingDevice => _connectingDevice;
|
||||
|
||||
/// 🔥 获取已连接设备的名称(连接时缓存,断开后为 null)
|
||||
String? get connectedDeviceName => _connectedDeviceName;
|
||||
Stream<List<ScanResult>> get scanResults => _scanController.stream;
|
||||
Stream<BlePacket> get packetStream => _packetController.stream;
|
||||
Stream<BluetoothAdapterState> get adapterState =>
|
||||
FlutterBluePlus.adapterState;
|
||||
|
||||
/// 🔥 获取当前蓝牙扫描结果列表(供外部查询使用)
|
||||
List<ScanResult> getScanResults() {
|
||||
return _scanResults.values.toList();
|
||||
}
|
||||
|
||||
/// 连接状态变化流:连接成功时发出 device,断开时发出 null
|
||||
Stream<BluetoothDevice?> get connectionStream => _connectionController.stream;
|
||||
|
||||
@@ -241,6 +252,18 @@ class BleManager {
|
||||
|
||||
/// 连接设备,返回 null 表示成功,返回错误信息字符串表示失败
|
||||
Future<String?> connect(BluetoothDevice device) async {
|
||||
// 🔥 连接前先保存设备名(从扫描结果或设备属性中获取)
|
||||
final scanResult = _scanResults[device.remoteId];
|
||||
final advName = scanResult?.device.advName ?? '';
|
||||
final platformName = device.platformName;
|
||||
_connectedDeviceName = platformName.isNotEmpty
|
||||
? platformName
|
||||
: (advName.isNotEmpty ? advName : device.remoteId.toString());
|
||||
developer.log(
|
||||
'[BLE] 🔥 保存已连接设备名: $_connectedDeviceName',
|
||||
name: 'BleManager',
|
||||
);
|
||||
|
||||
// 标记正在连接,通知所有监听者
|
||||
_connectingDevice = device;
|
||||
_connectingController.add(device);
|
||||
@@ -328,6 +351,7 @@ class BleManager {
|
||||
void _onDeviceDisconnected() {
|
||||
_connectedDevice = null;
|
||||
_connectingDevice = null;
|
||||
_connectedDeviceName = null; // 🔥 清空已连接设备名
|
||||
_writeCharacteristic = null;
|
||||
_readCharacteristic = null;
|
||||
_negotiatedMtu = 23;
|
||||
|
||||
@@ -143,7 +143,8 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
children: [
|
||||
PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
// 🔥 启用左右滑动切换 Tab
|
||||
physics: const BouncingScrollPhysics(),
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
|
||||
@@ -9,6 +9,7 @@ class RobotDataModel {
|
||||
final int statusCode; // 接口原始 status 状态码
|
||||
final double battery;
|
||||
final String task;
|
||||
final bool hasHost;
|
||||
|
||||
const RobotDataModel({
|
||||
required this.name,
|
||||
@@ -20,6 +21,7 @@ class RobotDataModel {
|
||||
this.statusCode = 0,
|
||||
required this.battery,
|
||||
required this.task,
|
||||
this.hasHost = false,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建数据模型
|
||||
@@ -46,6 +48,7 @@ class RobotDataModel {
|
||||
statusCode: statusCode is int ? statusCode : 0,
|
||||
battery: batteryValue,
|
||||
task: json['task'] ?? json['currentTask'] ?? '待机中',
|
||||
hasHost: json['hasHost'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,14 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
super.initState();
|
||||
_initConnectionListener();
|
||||
_syncInitialState();
|
||||
|
||||
// 🔥 页面加载完成后自动连接设备(如果尚未连接且未在连接中)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (!_isConnected && !_isConnecting) {
|
||||
_connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 监听 BleManager 的全局连接状态流
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
@@ -11,6 +12,7 @@ import '../../../../../components/tcp_status_indicator.dart';
|
||||
import '../../../../../components/device_status_modal.dart';
|
||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../../site/presentation/widgets/site_selector_widget.dart';
|
||||
import '../../../../main_container/domain/tab_config.dart';
|
||||
import '../bloc/device_status_bloc.dart' as DeviceListBloc;
|
||||
import '../bloc/device_status_event.dart' as DeviceListEvent;
|
||||
import '../bloc/device_status_state.dart' as DeviceListState;
|
||||
@@ -18,6 +20,7 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_statu
|
||||
import '../bloc/drone_station_bloc.dart';
|
||||
import '../bloc/drone_station_event.dart';
|
||||
import '../bloc/drone_station_state.dart';
|
||||
import 'ble_device_detail_page.dart';
|
||||
import '../bloc/robot_list_bloc.dart'; // 🔥 添加 RobotListBloc 导入
|
||||
import '../bloc/robot_list_event.dart'; // 🔥 添加 RobotListEvent 导入
|
||||
import '../bloc/robot_list_state.dart'; // 🔥 添加 RobotListState 导入
|
||||
@@ -74,6 +77,22 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 🔥 个人角色(roleKey == 'personal')只有一个场站,自动默认选中第一个
|
||||
final roleKey = sl<AppUserCubit>().state.user?.roleKey;
|
||||
if (roleKey == TabConfig.personalRoleKey) {
|
||||
final siteCubit = sl<SiteCubit>();
|
||||
if (siteCubit.state.sites.isEmpty) {
|
||||
// 场站列表为空则先加载
|
||||
siteCubit.loadSites();
|
||||
} else if (siteCubit.state.selectedSite == null) {
|
||||
// 还没有选中场站则自动选中第一个
|
||||
final firstSite = siteCubit.state.sites.first;
|
||||
debugPrint(
|
||||
'🔥 [DeviceStatus] 个人角色自动选中首个场站: ${firstSite.siteName} (id=${firstSite.id})',
|
||||
);
|
||||
siteCubit.selectSite(firstSite);
|
||||
}
|
||||
}
|
||||
_currentSiteId = sl<SiteCubit>().state.selectedSite?.id;
|
||||
_siteSub = sl<SiteCubit>().stream.listen((siteState) {
|
||||
if (!mounted) return;
|
||||
@@ -210,6 +229,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
}
|
||||
|
||||
Widget _buildAppBar(BuildContext context) {
|
||||
// 🔥 个人角色(roleKey == 'personal')隐藏场站选择器
|
||||
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||
final showSiteSelector = roleKey != TabConfig.personalRoleKey;
|
||||
|
||||
return Container(
|
||||
height: 44.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -218,8 +241,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
const Flexible(child: SiteSelectorWidget(compact: true)),
|
||||
const SizedBox(width: 8),
|
||||
if (showSiteSelector) ...[
|
||||
const Flexible(child: SiteSelectorWidget(compact: true)),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Container(
|
||||
width: 1,
|
||||
height: 20,
|
||||
@@ -397,24 +422,40 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
}
|
||||
|
||||
Widget _buildTypeFilterBar(BuildContext context) {
|
||||
final typeCodes = [
|
||||
'all',
|
||||
'robot',
|
||||
'drone_station',
|
||||
'inverter',
|
||||
'combiner_box',
|
||||
'module',
|
||||
'monitor',
|
||||
];
|
||||
final typeLabels = [
|
||||
AppLocalizations.of(context).translate('device_list_v2.all'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.inverter'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.combiner_box'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.module'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.monitor'),
|
||||
];
|
||||
// 🔥 个人角色(roleKey == 'personal')只显示「全部、机器人、无人机」3 个 tab
|
||||
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||
final isPersonal = roleKey == TabConfig.personalRoleKey;
|
||||
|
||||
List<String> typeCodes;
|
||||
List<String> typeLabels;
|
||||
|
||||
if (isPersonal) {
|
||||
typeCodes = ['all', 'robot', 'drone_station'];
|
||||
typeLabels = [
|
||||
AppLocalizations.of(context).translate('device_list_v2.all'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
||||
];
|
||||
} else {
|
||||
typeCodes = [
|
||||
'all',
|
||||
'robot',
|
||||
'drone_station',
|
||||
'inverter',
|
||||
'combiner_box',
|
||||
'module',
|
||||
'monitor',
|
||||
];
|
||||
typeLabels = [
|
||||
AppLocalizations.of(context).translate('device_list_v2.all'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.inverter'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.combiner_box'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.module'),
|
||||
AppLocalizations.of(context).translate('device_list_v2.monitor'),
|
||||
];
|
||||
}
|
||||
|
||||
return BlocBuilder<
|
||||
DeviceListBloc.DeviceStatusBloc,
|
||||
@@ -426,56 +467,81 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
selectedType = state.selectedType;
|
||||
}
|
||||
|
||||
// 获取当前选中的索引
|
||||
int selectedIndex = typeCodes.indexOf(selectedType);
|
||||
if (selectedIndex == -1) selectedIndex = 0; // 默认为 'all'
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
itemCount: typeCodes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
|
||||
itemBuilder: (context, index) {
|
||||
final typeCode = typeCodes[index];
|
||||
final typeLabel = typeLabels[index];
|
||||
final isSelected = selectedType == typeCode;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
// 🔥 监听滑动结束事件,自动切换选中状态
|
||||
if (notification is ScrollEndNotification &&
|
||||
notification.metrics.axis == Axis.horizontal) {
|
||||
final position = notification.metrics.pixels;
|
||||
final itemWidth = 56.0; // 预估每个 tab 的宽度(包括间距)
|
||||
final newIndex = (position / itemWidth).round();
|
||||
|
||||
if (newIndex >= 0 &&
|
||||
newIndex < typeCodes.length &&
|
||||
newIndex != selectedIndex) {
|
||||
final newTypeCode = typeCodes[newIndex];
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
DeviceListEvent.DeviceStatusChangeType(typeCode),
|
||||
DeviceListEvent.DeviceStatusChangeType(newTypeCode),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
typeLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: isSelected
|
||||
? context.appColors.primary
|
||||
: context.appColors.textSecondary,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.appColors.primary
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
itemCount: typeCodes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
|
||||
itemBuilder: (context, index) {
|
||||
final typeCode = typeCodes[index];
|
||||
final typeLabel = typeLabels[index];
|
||||
final isSelected = selectedType == typeCode;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
DeviceListEvent.DeviceStatusChangeType(typeCode),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
typeLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: isSelected
|
||||
? context.appColors.primary
|
||||
: context.appColors.textSecondary,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.appColors.primary
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -677,6 +743,36 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
debugPrint(
|
||||
'🔴🔴🔴 [全部-选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}',
|
||||
);
|
||||
debugPrint(
|
||||
'🔴🔴🔴 [全部-选中机器人] hasHost: ${robot.hasHost}',
|
||||
);
|
||||
|
||||
// 🔥 hasHost 字段判定:false → 蓝牙连接流程
|
||||
if (!robot.hasHost) {
|
||||
debugPrint('⚠️ [全部-EmbeddedRobotList] hasHost=false,进入蓝牙连接流程');
|
||||
|
||||
// 从设备名中提取时间戳(如 MC700PRO-CN-JS-1751525588120-0000000A-9527 → 1751525588120)
|
||||
final timestamp = _extractTimestamp(robot.name);
|
||||
if (timestamp == null || timestamp.isEmpty) {
|
||||
debugPrint('❌ [全部-EmbeddedRobotList] 未提取到时间戳');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('设备名称格式错误,无法提取时间戳'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [全部-EmbeddedRobotList] 提取到时间戳: $timestamp');
|
||||
|
||||
// 调用蓝牙连接流程
|
||||
_connectToDeviceViaBluetooth(context, robot.name, timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
// hasHost == true → 走原来的逻辑
|
||||
debugPrint('✅ [全部-EmbeddedRobotList] hasHost=true,走正常控制流程');
|
||||
|
||||
// 1. 将当前机器人设置为全局待控制设备
|
||||
final device = DeviceEntity(
|
||||
@@ -1234,4 +1330,177 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 从设备名中提取时间戳
|
||||
String? _extractTimestamp(String deviceName) {
|
||||
final regex = RegExp(r'(\d{13})');
|
||||
final match = regex.firstMatch(deviceName);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
/// 🔥 蓝牙连接流程
|
||||
Future<void> _connectToDeviceViaBluetooth(
|
||||
BuildContext context,
|
||||
String deviceName,
|
||||
String timestamp,
|
||||
) async {
|
||||
final bleManager = BleManager.instance;
|
||||
|
||||
// 🔥 0. 优先检查:如果该设备已经连接,直接跳转详情页,不再扫描
|
||||
final connectedDevice = bleManager.connectedDevice;
|
||||
if (connectedDevice != null) {
|
||||
final connectedName = bleManager.connectedDeviceName ?? '';
|
||||
debugPrint('🔍 [蓝牙连接] 已连接设备名: $connectedName,匹配时间戳: $timestamp');
|
||||
if (connectedName.contains(timestamp)) {
|
||||
debugPrint('✅ [蓝牙连接] 设备已连接,直接跳转详情页: $connectedName');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: connectedDevice),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 检查蓝牙状态
|
||||
final isBluetoothOn = await bleManager.checkBluetooth();
|
||||
if (!isBluetoothOn) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请先打开手机蓝牙'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 显示加载圈
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('正在搜索蓝牙设备...', style: TextStyle(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 3. 启动蓝牙扫描
|
||||
await bleManager.startScan(continuous: false);
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
|
||||
// 4. 关闭加载圈
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// 5. 获取扫描结果并查找匹配设备
|
||||
final scanResults = bleManager.getScanResults();
|
||||
BluetoothDevice? matchedDevice;
|
||||
for (final result in scanResults) {
|
||||
final devName = result.device.platformName.isNotEmpty
|
||||
? result.device.platformName
|
||||
: result.device.advName;
|
||||
if (devName.contains(timestamp)) {
|
||||
matchedDevice = result.device;
|
||||
debugPrint('✅ [蓝牙连接] 找到匹配设备: $devName');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedDevice != null) {
|
||||
debugPrint('🚀 [蓝牙连接] 跳转到 BLE 详情页并连接');
|
||||
final device = matchedDevice;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: device),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
debugPrint('⚠️ [蓝牙连接] 未在扫描列表中找到设备,弹窗提示并后台监测');
|
||||
_showNotFoundDialogAndMonitor(context, deviceName, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 弹窗提示并后台持续监测设备(单例模式,防止重复弹窗)
|
||||
void _showNotFoundDialogAndMonitor(
|
||||
BuildContext context,
|
||||
String deviceName,
|
||||
String timestamp,
|
||||
) {
|
||||
// 🔥 检查是否已有弹窗存在,如果有则先关闭
|
||||
if (Navigator.of(context).canPop()) {
|
||||
debugPrint('⚠️ [后台监测] 检测到已有弹窗,先关闭上一个');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
bool dialogShown = true;
|
||||
StreamSubscription? subscription;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
dialogShown = true;
|
||||
return AlertDialog(
|
||||
title: const Text('未找到设备'),
|
||||
content: const Text('请确认机器已上线并打开蓝牙了'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dialogShown = false;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dialogShown = false;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 启动后台监测
|
||||
final bleManager = BleManager.instance;
|
||||
subscription = bleManager.scanResults.listen((results) {
|
||||
if (!dialogShown) {
|
||||
subscription?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
for (final result in results) {
|
||||
final devName = result.device.platformName.isEmpty
|
||||
? result.device.advName
|
||||
: result.device.platformName;
|
||||
|
||||
if (devName.contains(timestamp)) {
|
||||
debugPrint('✅ [后台监测] 匹配成功!设备名: $devName');
|
||||
subscription?.cancel();
|
||||
|
||||
// 🔥 弹窗可能已关闭,需要先 pop 当前弹窗再跳转
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: result.device),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/bluetooth/ble_manager.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../../devices/presentation/bloc/device_status_bloc.dart';
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../data/models/robot_data_model.dart';
|
||||
@@ -14,6 +19,7 @@ import '../bloc/robot_list_bloc.dart';
|
||||
import '../bloc/robot_list_event.dart';
|
||||
import '../bloc/robot_list_state.dart';
|
||||
import '../widgets/robot_item_card.dart';
|
||||
import 'ble_device_detail_page.dart';
|
||||
import 'robot_control_page.dart';
|
||||
import 'cleaning_weeding_robot_task_page.dart';
|
||||
|
||||
@@ -87,7 +93,8 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
children: [
|
||||
_buildStatsCard(state),
|
||||
_buildQuickActions(state),
|
||||
_buildCurrentTask(state),
|
||||
// 🔥 个人角色暂时注释掉当前任务卡片
|
||||
// _buildCurrentTask(state),
|
||||
..._buildRobotList(state),
|
||||
],
|
||||
),
|
||||
@@ -646,6 +653,38 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
debugPrint(
|
||||
'🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}',
|
||||
);
|
||||
debugPrint(
|
||||
'🔴🔴🔴 [选中机器人] hasHost: ${robot.hasHost}',
|
||||
);
|
||||
|
||||
// 🔥 hasHost 字段判定:false → 蓝牙连接流程,true 走原逻辑
|
||||
if (!robot.hasHost) {
|
||||
// hasHost == false 或接口未返回该字段 → 蓝牙连接流程
|
||||
debugPrint('⚠️ [RobotListPage] hasHost=false,进入蓝牙连接流程');
|
||||
debugPrint('📱 [RobotListPage] 机器人名称: ${robot.name}');
|
||||
|
||||
// 从设备名中提取时间戳(例如:MC700PRO-CN-JS-1751525588120-0000000A-9527)
|
||||
final timestamp = _extractTimestamp(robot.name);
|
||||
if (timestamp == null || timestamp.isEmpty) {
|
||||
debugPrint('❌ [RobotListPage] 无法从设备名中提取时间戳: ${robot.name}');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('设备名格式异常,无法进行蓝牙连接'),
|
||||
backgroundColor: context.appColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [RobotListPage] 提取到时间戳: $timestamp');
|
||||
|
||||
// 调用蓝牙连接流程
|
||||
_connectToDeviceViaBluetooth(context, robot.name, timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
// hasHost == true → 走原来的逻辑
|
||||
debugPrint('✅ [RobotListPage] hasHost=true,走正常控制流程');
|
||||
|
||||
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
|
||||
final device = DeviceEntity(
|
||||
@@ -687,4 +726,219 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 🔥 从设备名中提取时间戳
|
||||
/// 例如:MC700PRO-CN-JS-1751525588120-0000000A-9527 → 1751525588120
|
||||
String? _extractTimestamp(String deviceName) {
|
||||
try {
|
||||
// 使用正则表达式匹配中间的一串数字(13位时间戳)
|
||||
final regex = RegExp(r'(\d{13})');
|
||||
final match = regex.firstMatch(deviceName);
|
||||
if (match != null) {
|
||||
return match.group(1);
|
||||
}
|
||||
debugPrint('⚠️ [_extractTimestamp] 未找到时间戳格式,返回 null');
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [_extractTimestamp] 解析失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 蓝牙连接流程
|
||||
/// 1. 检查蓝牙状态
|
||||
/// 2. 在 BLE 扫描列表中查找包含时间戳的设备
|
||||
/// 3. 如果找到则连接并跳转详情页
|
||||
/// 4. 如果没找到则弹窗提示并后台持续监测
|
||||
Future<void> _connectToDeviceViaBluetooth(
|
||||
BuildContext context,
|
||||
String deviceName,
|
||||
String timestamp,
|
||||
) async {
|
||||
final bleManager = BleManager.instance;
|
||||
|
||||
// 🔥 0. 优先检查:如果该设备已经连接,直接跳转详情页,不再扫描
|
||||
final connectedDevice = bleManager.connectedDevice;
|
||||
if (connectedDevice != null) {
|
||||
final connectedName = bleManager.connectedDeviceName ?? '';
|
||||
debugPrint('🔍 [蓝牙连接] 已连接设备名: $connectedName,匹配时间戳: $timestamp');
|
||||
if (connectedName.contains(timestamp)) {
|
||||
debugPrint('✅ [蓝牙连接] 设备已连接,直接跳转详情页: $connectedName');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: connectedDevice),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 检查蓝牙是否打开
|
||||
final isBluetoothOn = await bleManager.checkBluetooth();
|
||||
if (!isBluetoothOn) {
|
||||
debugPrint('❌ [蓝牙连接] 蓝牙未打开,提示用户');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('请先打开蓝牙'),
|
||||
backgroundColor: context.appColors.warning,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [蓝牙连接] 蓝牙已打开,开始搜索设备: $deviceName');
|
||||
|
||||
// 2. 显示加载圈
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('正在搜索蓝牙设备...', style: TextStyle(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// 3. 启动蓝牙扫描(只扫一次,不要连续扫)
|
||||
try {
|
||||
await bleManager.startScan(continuous: false);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [蓝牙连接] 启动扫描失败: $e');
|
||||
}
|
||||
|
||||
// 4. 等待一下让扫描结果出现
|
||||
await Future.delayed(const Duration(milliseconds: 1500));
|
||||
|
||||
// 5. 关闭加载圈
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// 6. 获取当前扫描结果列表
|
||||
final scanResults = bleManager.getScanResults();
|
||||
debugPrint('🔍 [蓝牙连接] 扫描到 ${scanResults.length} 个设备');
|
||||
|
||||
// 5. 查找包含时间戳的设备
|
||||
BluetoothDevice? matchedDevice;
|
||||
for (final result in scanResults) {
|
||||
final dev = result.device;
|
||||
final devName = dev.platformName.isEmpty
|
||||
? (dev.advName.isEmpty ? dev.remoteId.toString() : dev.advName)
|
||||
: dev.platformName;
|
||||
|
||||
debugPrint(' 📱 发现设备: $devName (包含时间戳?: ${devName.contains(timestamp)})');
|
||||
|
||||
// 检查设备名是否包含时间戳
|
||||
if (devName.contains(timestamp)) {
|
||||
debugPrint('✅ [蓝牙连接] 匹配成功!设备名: $devName');
|
||||
matchedDevice = dev;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedDevice != null) {
|
||||
debugPrint('🚀 [蓝牙连接] 跳转到 BLE 详情页并连接');
|
||||
// 跳转到 BLE 设备详情页(详情页会自动连接)
|
||||
final device = matchedDevice;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: device),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
debugPrint('⚠️ [蓝牙连接] 未在扫描列表中找到设备,弹窗提示并后台监测');
|
||||
// 6. 没找到设备,弹窗提示并后台持续监测
|
||||
_showNotFoundDialogAndMonitor(context, deviceName, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 弹窗提示并后台持续监测设备(单例模式,防止重复弹窗)
|
||||
void _showNotFoundDialogAndMonitor(
|
||||
BuildContext context,
|
||||
String deviceName,
|
||||
String timestamp,
|
||||
) {
|
||||
// 🔥 检查是否已有弹窗存在,如果有则先关闭
|
||||
if (Navigator.of(context).canPop()) {
|
||||
debugPrint('⚠️ [后台监测] 检测到已有弹窗,先关闭上一个');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
bool dialogShown = true;
|
||||
StreamSubscription? subscription;
|
||||
|
||||
// 先显示对话框
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
dialogShown = true;
|
||||
return AlertDialog(
|
||||
title: const Text('未找到设备'),
|
||||
content: const Text('请确认机器已上线并打开蓝牙了'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dialogShown = false;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
dialogShown = false;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
).then((_) {
|
||||
// 对话框关闭后不停止监测,继续运行
|
||||
});
|
||||
|
||||
// 启动后台监测 BLE 扫描列表
|
||||
final bleManager = BleManager.instance;
|
||||
subscription = bleManager.scanResults.listen((results) {
|
||||
if (!dialogShown) {
|
||||
// 对话框已关闭,停止监测
|
||||
subscription?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔍 [后台监测] 扫描到 ${results.length} 个设备,搜索时间戳: $timestamp');
|
||||
|
||||
for (final result in results) {
|
||||
final dev = result.device;
|
||||
final devName = dev.platformName.isEmpty
|
||||
? (dev.advName.isEmpty ? dev.remoteId.toString() : dev.advName)
|
||||
: dev.platformName;
|
||||
|
||||
if (devName.contains(timestamp)) {
|
||||
debugPrint('✅ [后台监测] 匹配成功!设备名: $devName');
|
||||
subscription?.cancel();
|
||||
|
||||
// 🔥 弹窗可能已关闭,需要先 pop 当前弹窗再跳转
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BleDeviceDetailPage(device: dev!),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user