Commit 192f8476 authored by akari's avatar akari

fix: 修复蓝牙相关

parent d697b832
......@@ -28,19 +28,26 @@ class LakiMqttConfig {
this.logging = false,
});
/// TODO: 替换为腾讯云 IoT MQTT broker 地址、端口、clientId 和鉴权信息。
const LakiMqttConfig.placeholder()
: host = '',
port = 1883,
clientId = '',
username = null,
password = null,
useSsl = false,
useWebSocket = false,
webSocketProtocols = null,
keepAliveSeconds = 30,
connectTimeoutMs = 5000,
logging = false;
factory LakiMqttConfig.tencentTdmq({
required String deviceSn,
bool logging = false,
}) {
return LakiMqttConfig(
host: _tencentTdmqHost,
port: _tencentTdmqPort,
clientId: '$_tencentTdmqClientIdPrefix$deviceSn',
username: _tencentTdmqUsername,
password: _tencentTdmqPassword,
logging: logging,
);
}
static const String _tencentTdmqHost =
'mqtt-7jqojva8-sh-public.mqtt.tencenttdmq.com';
static const int _tencentTdmqPort = 1883;
static const String _tencentTdmqClientIdPrefix = 'GID_icu@@@';
static const String _tencentTdmqUsername = 'root';
static const String _tencentTdmqPassword = 'sk665f2b219cb3700b';
final String host;
final int port;
......
......@@ -6,6 +6,8 @@ import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/p2p_video_service.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:laki_icu_app/utils/storage/storage_service.dart';
import 'monitoring_index_state.dart';
......@@ -16,9 +18,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final MonitoringService _monitoringService;
final WebrtcService _webrtcService;
final P2pVideoService _p2pVideoService;
final StorageService _storageService;
final BleBluetoothManager _bluetoothManager;
final McuReportFrameDecoder _mcuReportDecoder;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
StreamSubscription<P2pServiceState>? _p2pStateSub;
StreamSubscription<List<BluetoothScanDevice>>? _bluetoothScanSub;
StreamSubscription<BluetoothConnectionStatus>? _bluetoothStateSub;
StreamSubscription<String>? _bluetoothMessageSub;
StreamSubscription<BluetoothDataPacket>? _bluetoothDataSub;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
......@@ -28,9 +37,14 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
: _monitoringService = MonitoringService(),
_webrtcService = WebrtcService(),
_p2pVideoService = P2pVideoService(),
_storageService = StorageService(),
_bluetoothManager = BleBluetoothManager(),
_mcuReportDecoder = McuReportFrameDecoder(),
super(const MonitoringIndexState()) {
_listenWebrtcState();
_listenP2pState();
_listenBluetoothState();
_loadBoundBluetoothDevice();
loadData();
}
......@@ -47,11 +61,69 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_p2pStateSub = _p2pVideoService.stateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(
isP2pConnected: state == P2pServiceState.connected,
));
isP2pConnected: state == P2pServiceState.connected,
));
});
}
/// 监听蓝牙扫描、连接和数据状态
void _listenBluetoothState() {
_bluetoothScanSub = _bluetoothManager.scanDevicesStream.listen((devices) {
if (isClosed) return;
emit(state.copyWith(bluetoothDevices: devices));
});
_bluetoothStateSub = _bluetoothManager.stateStream.listen((status) {
if (isClosed) return;
emit(state.copyWith(bluetoothConnectionStatus: status));
});
_bluetoothMessageSub = _bluetoothManager.messageStream.listen((message) {
if (isClosed) return;
emit(state.copyWith(bluetoothMessage: message));
});
_bluetoothDataSub = _bluetoothManager.dataStream.listen((packet) {
if (isClosed) return;
try {
final reports = _mcuReportDecoder.addBytes(packet.bytes);
final latestReport = reports.isEmpty ? null : reports.last;
emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex,
latestMcuReport: latestReport,
bluetoothMessage: latestReport == null
? state.bluetoothMessage
: '蓝牙数据解析成功: ${latestReport.sn.isEmpty ? latestReport.head : latestReport.sn}',
));
} catch (e) {
emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex,
bluetoothMessage: '蓝牙数据解析失败: $e',
));
}
});
}
Future<void> _loadBoundBluetoothDevice() async {
final boundDevice = await _storageService.getBoundBluetoothDevice();
if (isClosed) return;
final deviceId = boundDevice['deviceId'];
if (deviceId != null && deviceId.isNotEmpty) {
final userMac = await _storageService.getUserMac();
if (userMac == null || userMac.isEmpty) {
await _storageService.saveUserMac(deviceId);
}
} else {
await _storageService.getUserMac();
}
if (isClosed) return;
emit(state.copyWith(
boundBluetoothDeviceId:
deviceId == null || deviceId.isEmpty ? null : deviceId,
boundBluetoothDeviceName: boundDevice['deviceName'],
));
}
Future<void> loadData() async {
emit(state.copyWith(
status: MonitoringIndexStatus.loading,
......@@ -204,6 +276,81 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
}
// ==================== 蓝牙扫描/绑定 ====================
Future<void> scanBluetoothDevices() async {
if (state.isBluetoothScanning) return;
emit(state.copyWith(
isBluetoothScanning: true,
bluetoothDevices: const [],
bluetoothMessage: '开始扫描蓝牙设备',
));
try {
await _bluetoothManager.scanForDevices(
namePrefixes: const [],
timeout: const Duration(seconds: 10),
);
} catch (e) {
if (isClosed) return;
emit(state.copyWith(bluetoothMessage: '蓝牙扫描失败: $e'));
} finally {
if (!isClosed) {
emit(state.copyWith(isBluetoothScanning: false));
}
}
}
Future<void> bindBluetoothDevice(BluetoothScanDevice device) async {
if (state.isBluetoothBinding) return;
_mcuReportDecoder.clear();
emit(state.copyWith(
isBluetoothBinding: true,
bindingBluetoothDeviceId: device.remoteId,
bluetoothMessage: '正在绑定蓝牙设备: ${device.name}',
));
try {
await _bluetoothManager.connectToDevice(device);
await _storageService.saveBoundBluetoothDevice(
deviceId: device.remoteId,
deviceName: device.name,
);
await _storageService.saveUserMac(device.remoteId);
if (isClosed) return;
emit(state.copyWith(
isBluetoothBinding: false,
bindingBluetoothDeviceId: '',
boundBluetoothDeviceId: device.remoteId,
boundBluetoothDeviceName: device.name,
bluetoothMessage: '蓝牙设备已绑定: ${device.name}',
));
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
isBluetoothBinding: false,
bindingBluetoothDeviceId: '',
bluetoothMessage: '蓝牙绑定失败: $e',
));
}
}
Future<void> unbindBluetoothDevice() async {
await _bluetoothManager.disconnect();
await _storageService.deleteBoundBluetoothDevice();
await _storageService.deleteUserMac();
_mcuReportDecoder.clear();
if (isClosed) return;
emit(state.copyWith(
clearBoundBluetoothDevice: true,
clearLatestMcuReport: true,
bluetoothMessage: '蓝牙设备已解绑',
latestBluetoothRawHex: '',
));
}
/// 获取 WebRTC 渲染器供 UI 层使用
WebrtcService get webrtcService => _webrtcService;
......@@ -214,8 +361,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
Future<void> close() {
_webrtcStateSub?.cancel();
_p2pStateSub?.cancel();
_bluetoothScanSub?.cancel();
_bluetoothStateSub?.cancel();
_bluetoothMessageSub?.cancel();
_bluetoothDataSub?.cancel();
_webrtcService.dispose();
_p2pVideoService.dispose();
return super.close();
return _bluetoothManager.release().then((_) => super.close());
}
}
......@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
enum MonitoringIndexStatus {
initial,
......@@ -31,6 +32,36 @@ class MonitoringIndexState extends Equatable {
/// P2P 模式是否已连接
final bool isP2pConnected;
/// 蓝牙扫描中
final bool isBluetoothScanning;
/// 蓝牙绑定/连接处理中
final bool isBluetoothBinding;
/// 正在绑定/连接的蓝牙设备 ID
final String bindingBluetoothDeviceId;
/// 扫描到的蓝牙设备
final List<BluetoothScanDevice> bluetoothDevices;
/// 已绑定蓝牙设备 ID
final String? boundBluetoothDeviceId;
/// 已绑定蓝牙设备名称
final String? boundBluetoothDeviceName;
/// 蓝牙连接状态
final BluetoothConnectionStatus bluetoothConnectionStatus;
/// 蓝牙状态提示
final String? bluetoothMessage;
/// 最近收到的蓝牙原始 Hex 数据
final String? latestBluetoothRawHex;
/// 最近解析出的 MCU 上报数据
final McuReport? latestMcuReport;
const MonitoringIndexState({
this.status = MonitoringIndexStatus.initial,
this.isLoading = false,
......@@ -43,6 +74,16 @@ class MonitoringIndexState extends Equatable {
this.videoStreamMode = VideoStreamMode.p2p,
this.isSwitchingMode = false,
this.isP2pConnected = false,
this.isBluetoothScanning = false,
this.isBluetoothBinding = false,
this.bindingBluetoothDeviceId = '',
this.bluetoothDevices = const [],
this.boundBluetoothDeviceId,
this.boundBluetoothDeviceName,
this.bluetoothConnectionStatus = BluetoothConnectionStatus.disconnected,
this.bluetoothMessage,
this.latestBluetoothRawHex,
this.latestMcuReport,
});
MonitoringIndexState copyWith({
......@@ -57,6 +98,18 @@ class MonitoringIndexState extends Equatable {
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
bool? isP2pConnected,
bool? isBluetoothScanning,
bool? isBluetoothBinding,
String? bindingBluetoothDeviceId,
List<BluetoothScanDevice>? bluetoothDevices,
String? boundBluetoothDeviceId,
String? boundBluetoothDeviceName,
BluetoothConnectionStatus? bluetoothConnectionStatus,
String? bluetoothMessage,
String? latestBluetoothRawHex,
McuReport? latestMcuReport,
bool clearBoundBluetoothDevice = false,
bool clearLatestMcuReport = false,
}) {
return MonitoringIndexState(
status: status ?? this.status,
......@@ -66,11 +119,28 @@ class MonitoringIndexState extends Equatable {
patientInfo: patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
menuItems: menuItems ?? this.menuItems,
videoConnectionState:
videoConnectionState ?? this.videoConnectionState,
videoConnectionState: videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
isP2pConnected: isP2pConnected ?? this.isP2pConnected,
isBluetoothScanning: isBluetoothScanning ?? this.isBluetoothScanning,
isBluetoothBinding: isBluetoothBinding ?? this.isBluetoothBinding,
bindingBluetoothDeviceId:
bindingBluetoothDeviceId ?? this.bindingBluetoothDeviceId,
bluetoothDevices: bluetoothDevices ?? this.bluetoothDevices,
boundBluetoothDeviceId: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceId ?? this.boundBluetoothDeviceId,
boundBluetoothDeviceName: clearBoundBluetoothDevice
? null
: boundBluetoothDeviceName ?? this.boundBluetoothDeviceName,
bluetoothConnectionStatus:
bluetoothConnectionStatus ?? this.bluetoothConnectionStatus,
bluetoothMessage: bluetoothMessage ?? this.bluetoothMessage,
latestBluetoothRawHex:
latestBluetoothRawHex ?? this.latestBluetoothRawHex,
latestMcuReport:
clearLatestMcuReport ? null : latestMcuReport ?? this.latestMcuReport,
);
}
......@@ -87,5 +157,15 @@ class MonitoringIndexState extends Equatable {
videoStreamMode,
isSwitchingMode,
isP2pConnected,
isBluetoothScanning,
isBluetoothBinding,
bindingBluetoothDeviceId,
bluetoothDevices,
boundBluetoothDeviceId,
boundBluetoothDeviceName,
bluetoothConnectionStatus,
bluetoothMessage,
latestBluetoothRawHex,
latestMcuReport,
];
}
......@@ -7,6 +7,7 @@ import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'cubit/monitoring_index_cubit.dart';
import 'cubit/monitoring_index_state.dart';
import 'widgets/bluetooth_bind_dialog.dart';
import 'widgets/monitoring_alert_card.dart';
import 'widgets/monitoring_bottom_menu.dart';
import 'widgets/monitoring_metric_card.dart';
......@@ -95,24 +96,25 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
Container(
height: topHeight.clamp(80.0, 104.0),
decoration: BoxDecoration(
border: BoxBorder.all(width: 1.w,color: Colors.red)
),
child: const MonitoringTopStatusBar(),
border: BoxBorder.all(width: 1.w, color: Colors.red)),
child: MonitoringTopStatusBar(
isBluetoothBound:
state.boundBluetoothDeviceId?.isNotEmpty == true,
onBluetoothTap: _showBluetoothDialog,
),
),
Expanded(
child: Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
border: BoxBorder.all(width: 1.w,color: Colors.red)
),
border: BoxBorder.all(width: 1.w, color: Colors.red)),
child: _buildMainArea(state, gap),
),
),
Container(
height: bottomHeight.clamp(92.0, 134.0),
decoration: BoxDecoration(
border: BoxBorder.all(width: 1.w,color: Colors.red)
),
border: BoxBorder.all(width: 1.w, color: Colors.red)),
child: MonitoringBottomMenu(menuItems: state.menuItems),
),
],
......@@ -142,7 +144,6 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
child: _buildMetrics(state.metrics, gap),
),
// SizedBox(width: gap),
],
),
),
......@@ -190,8 +191,7 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
child: Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
border: BoxBorder.all(width: 1.w,color: Colors.red)
),
border: BoxBorder.all(width: 1.w, color: Colors.red)),
// padding: EdgeInsets.only(
// right: metric == displayMetrics.last ? 0 : gap,
// ),
......@@ -213,6 +213,16 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
cubit.switchStreamMode(nextMode);
}
void _showBluetoothDialog() {
showDialog<void>(
context: context,
builder: (_) => BlocProvider.value(
value: _monitoringIndexCubit,
child: const BluetoothBindDialog(),
),
);
}
List<MonitoringMetricBO> _environmentMetrics(
List<MonitoringMetricBO> metrics) {
final matched = metrics
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:laki_icu_app/views/monitoring/index/cubit/monitoring_index_cubit.dart';
import 'package:laki_icu_app/views/monitoring/index/cubit/monitoring_index_state.dart';
class BluetoothBindDialog extends StatefulWidget {
const BluetoothBindDialog({super.key});
@override
State<BluetoothBindDialog> createState() => _BluetoothBindDialogState();
}
class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
bool _hasAutoStartedScan = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _hasAutoStartedScan) return;
_hasAutoStartedScan = true;
final cubit = context.read<MonitoringIndexCubit>();
if (!cubit.state.isBluetoothScanning) {
cubit.scanBluetoothDevices();
}
});
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: const Color(0xFFF4F4F4),
insetPadding: EdgeInsets.zero,
child: BlocBuilder<MonitoringIndexCubit, MonitoringIndexState>(
builder: (context, state) {
return SizedBox.expand(
child: _buildContent(context, state),
);
},
),
);
}
Widget _buildContent(BuildContext context, MonitoringIndexState state) {
return Container(
width: double.infinity,
// margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
padding: EdgeInsets.fromLTRB(108.w, 52.h, 108.w, 94.h),
decoration: BoxDecoration(
color: const Color(0xFF79A9EE),
border: Border.all(color: const Color(0xFF0799FF), width: 3.w),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopBar(context),
SizedBox(height: 38.h),
Expanded(child: _buildDevicePanel(context, state)),
],
),
);
}
Widget _buildTopBar(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备连接',
style: TextStyle(
color: Colors.white,
fontSize: 42.sp,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 14.h),
Text(
'搜索蓝牙设备完成配对连接',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.84),
fontSize: 22.sp,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.close, color: Colors.white, size: 48.w),
tooltip: '关闭',
padding: EdgeInsets.zero,
constraints: BoxConstraints.tight(Size(64.w, 64.w)),
),
],
);
}
Widget _buildDevicePanel(BuildContext context, MonitoringIndexState state) {
return Container(
width: double.infinity,
padding: EdgeInsets.fromLTRB(72.w, 56.h, 72.w, 34.h),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0028E8), Color(0xFF001646)],
),
borderRadius: BorderRadius.circular(14.r),
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.26),
blurRadius: 12.r,
offset: Offset(0, 6.h),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPanelStatus(context, state),
SizedBox(height: 28.h),
Expanded(child: _buildDeviceList(context, state)),
_buildLatestData(state),
],
),
);
}
Widget _buildPanelStatus(BuildContext context, MonitoringIndexState state) {
final message = state.isBluetoothScanning
? '自动发现附近可配对的监护舱设备,搜索中...'
: state.bluetoothDevices.isEmpty
? '未发现设备,可重新搜索附近蓝牙设备'
: '自动发现附近可配对的监护舱设备';
return Row(
children: [
Expanded(
child: Text(
message,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.88),
fontSize: 21.sp,
fontWeight: FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 16.w),
TextButton.icon(
onPressed: state.isBluetoothScanning
? null
: () =>
context.read<MonitoringIndexCubit>().scanBluetoothDevices(),
icon: state.isBluetoothScanning
? SizedBox(
width: 18.w,
height: 18.w,
child: CircularProgressIndicator(
strokeWidth: 2.w,
color: Colors.white,
),
)
: Icon(Icons.refresh, size: 22.w),
label: Text(state.isBluetoothScanning ? '搜索中' : '重新搜索'),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white70,
textStyle: TextStyle(fontSize: 17.sp),
),
),
],
);
}
Widget _buildDeviceList(BuildContext context, MonitoringIndexState state) {
if (state.bluetoothDevices.isEmpty) {
return Center(
child: Text(
state.isBluetoothScanning ? '正在扫描附近设备...' : '暂无设备',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.76),
fontSize: 24.sp,
),
),
);
}
final devices = _sortDevicesForDisplay(state);
return ListView.separated(
padding: EdgeInsets.zero,
itemCount: devices.length,
separatorBuilder: (_, __) => SizedBox(height: 18.h),
itemBuilder: (context, index) {
final device = devices[index];
final isBound = state.boundBluetoothDeviceId == device.remoteId;
final isBinding = state.bindingBluetoothDeviceId == device.remoteId;
return _BluetoothDeviceRow(
key: ValueKey(device.remoteId),
device: device,
isBound: isBound,
isBinding: isBinding,
isDisabled: state.isBluetoothBinding,
onTap: isBound
? () => _confirmUnbindBluetoothDevice(context, device)
: () => context.read<MonitoringIndexCubit>().bindBluetoothDevice(
device,
),
);
},
);
}
List<BluetoothScanDevice> _sortDevicesForDisplay(MonitoringIndexState state) {
final boundDeviceId = state.boundBluetoothDeviceId;
if (boundDeviceId == null || boundDeviceId.isEmpty) {
return state.bluetoothDevices;
}
return List<BluetoothScanDevice>.from(state.bluetoothDevices)
..sort((left, right) {
final leftIsBound = left.remoteId == boundDeviceId;
final rightIsBound = right.remoteId == boundDeviceId;
if (leftIsBound == rightIsBound) {
return 0;
}
return leftIsBound ? -1 : 1;
});
}
Future<void> _confirmUnbindBluetoothDevice(
BuildContext context,
BluetoothScanDevice device,
) async {
final shouldUnbind = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('解绑蓝牙设备'),
content: Text('是否要解绑 ${device.name}?解绑后可重新选择设备绑定。'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('确认解绑'),
),
],
);
},
);
if (shouldUnbind == true && context.mounted) {
await context.read<MonitoringIndexCubit>().unbindBluetoothDevice();
}
}
Widget _buildLatestData(MonitoringIndexState state) {
final report = state.latestMcuReport;
final rawHex = state.latestBluetoothRawHex;
final message = state.bluetoothMessage;
if (report == null &&
(rawHex == null || rawHex.isEmpty) &&
(message == null || message.isEmpty)) {
return SizedBox(height: 10.h);
}
if (report != null) {
return _BluetoothReportSummary(report: report);
}
return Padding(
padding: EdgeInsets.only(top: 18.h),
child: Text(
rawHex != null && rawHex.isNotEmpty ? '最新数据: $rawHex' : message!,
style: TextStyle(
color: rawHex != null && rawHex.isNotEmpty
? const Color(0xFF00F6FF)
: Colors.white.withValues(alpha: 0.66),
fontSize: 15.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
}
class _BluetoothReportSummary extends StatelessWidget {
const _BluetoothReportSummary({required this.report});
final McuReport report;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: EdgeInsets.only(top: 18.h),
padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 14.h),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
report.sn.isEmpty ? 'SN --' : 'SN ${report.sn}',
style: TextStyle(
color: const Color(0xFF00F6FF),
fontSize: 15.sp,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
_BluetoothReportMetric(
label: '温度',
value: report.mainTemperature.toStringAsFixed(1),
unit: '℃',
),
_BluetoothReportMetric(
label: '湿度',
value: '${report.humidity}',
unit: '%',
),
_BluetoothReportMetric(
label: '氧气',
value: '${report.oxygenConcentration}',
unit: '%',
),
_BluetoothReportMetric(
label: 'CO2',
value: '${report.co2Measurement}',
unit: 'ppm',
),
],
),
);
}
}
class _BluetoothReportMetric extends StatelessWidget {
const _BluetoothReportMetric({
required this.label,
required this.value,
required this.unit,
});
final String label;
final String value;
final String unit;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 128.w,
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: '$label ',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.58),
fontSize: 13.sp,
),
),
TextSpan(
text: value,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
TextSpan(
text: unit,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
fontSize: 12.sp,
),
),
],
),
),
);
}
}
class _BluetoothDeviceRow extends StatelessWidget {
const _BluetoothDeviceRow({
super.key,
required this.device,
required this.isBound,
required this.isBinding,
required this.isDisabled,
required this.onTap,
});
final BluetoothScanDevice device;
final bool isBound;
final bool isBinding;
final bool isDisabled;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
const highlightColor = Color(0xFF00F6FF);
final textColor = isBound ? highlightColor : Colors.white;
final statusText = isBound
? '已连接'
: isBinding
? '连接中'
: '未连接';
return InkWell(
onTap: isDisabled ? null : onTap,
borderRadius: BorderRadius.circular(8.r),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 14.h),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.name,
style: TextStyle(
color: textColor,
fontSize: 26.sp,
fontWeight: isBound ? FontWeight.w600 : FontWeight.w400,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 5.h),
Text(
'${device.remoteId} RSSI ${device.rssi}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.42),
fontSize: 13.sp,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
SizedBox(width: 32.w),
SizedBox(
width: 138.w,
child: Text(
statusText,
textAlign: TextAlign.left,
style: TextStyle(
color: textColor,
fontSize: 25.sp,
fontWeight: isBound ? FontWeight.w600 : FontWeight.w400,
),
),
),
],
),
),
);
}
}
......@@ -2,7 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class MonitoringTopStatusBar extends StatelessWidget {
const MonitoringTopStatusBar({super.key});
const MonitoringTopStatusBar({
super.key,
this.isBluetoothBound = false,
this.onBluetoothTap,
});
final bool isBluetoothBound;
final VoidCallback? onBluetoothTap;
@override
Widget build(BuildContext context) {
......@@ -13,15 +20,15 @@ class MonitoringTopStatusBar extends StatelessWidget {
fit: BoxFit.fill,
),
),
padding: EdgeInsets.only(left: 20.w,bottom: 22.h),
padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 22.h),
child: Row(
children: [
children: [
// SizedBox(width: 18.w),
Text(
'总运行时长:9999h',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontSize: 16.sp,
),
),
SizedBox(width: 8.w),
......@@ -29,7 +36,7 @@ class MonitoringTopStatusBar extends StatelessWidget {
'环境温度:30.68',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontSize: 16.sp,
),
),
SizedBox(width: 8.w),
......@@ -37,14 +44,64 @@ class MonitoringTopStatusBar extends StatelessWidget {
'总制氧时长:500h',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontSize: 16.sp,
),
),
// const Spacer(),
const Spacer(),
_BluetoothStatusButton(
isActive: isBluetoothBound,
onTap: onBluetoothTap,
),
],
),
);
}
}
class _BluetoothStatusButton extends StatelessWidget {
const _BluetoothStatusButton({
required this.isActive,
required this.onTap,
});
final bool isActive;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final foregroundColor = isActive ? const Color(0xFF003A8C) : Colors.white;
final backgroundColor = isActive
? const Color(0xFF00F6FF)
: Colors.white.withValues(alpha: 0.08);
final borderColor = isActive ? const Color(0xFFB8FFFF) : Colors.white24;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18.r),
child: Container(
height: 34.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor, width: 1),
borderRadius: BorderRadius.circular(18.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bluetooth, color: foregroundColor, size: 18.w),
SizedBox(width: 6.w),
Text(
'TSAAS',
style: TextStyle(
color: foregroundColor,
fontSize: 14.sp,
fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
),
),
],
),
),
);
}
}
......@@ -59,6 +59,7 @@ dependencies:
path: ./plugins/vsdk
flutter_reactive_ble: ^5.5.0
mqtt_client: ^10.5.1
marionette_flutter: ^0.5.0
dev_dependencies:
flutter_test:
sdk: flutter
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment