Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
laki_icu_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
laki_icu_app
Commits
ddc45594
Commit
ddc45594
authored
Jun 23, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
cc 合并到 master
parent
f0145cb5
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
2 additions
and
1730 deletions
+2
-1730
usb_camera_service.dart
lib/services/usb_camera_service.dart
+0
-337
home_index_cubit.dart
lib/views/home/index/cubit/home_index_cubit.dart
+0
-366
home_index_state.dart
lib/views/home/index/cubit/home_index_state.dart
+0
-171
home_index_view.dart
lib/views/home/index/home_index_view.dart
+0
-369
bluetooth_bind_dialog.dart
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
+0
-485
pubspec.yaml
pubspec.yaml
+2
-2
No files found.
lib/services/usb_camera_service.dart
deleted
100644 → 0
View file @
f0145cb5
import
'dart:async'
;
import
'dart:io'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter_usbcamera/flutter_usbcamera.dart'
;
import
'package:path_provider/path_provider.dart'
;
/// USB 摄像头服务层
///
/// 职责:
/// - 封装 [UsbCameraController],对上层提供统一的业务接口
/// - 监听设备热插拔事件,自动管理摄像头生命周期
/// - 将事件流中的拍摄完成回调转换为 Future 返回值
///
/// 涉及页面:首页监护舱 - 监控画面
class
UsbCameraService
{
static
UsbCameraService
?
_instance
;
final
UsbCameraController
_controller
;
StreamSubscription
<
CameraEvent
>?
_eventSub
;
// 事件广播流(供 UI 层订阅设备状态变化)
final
StreamController
<
CameraEvent
>
_eventController
=
StreamController
<
CameraEvent
>.
broadcast
();
// 拍照/录像完成时的 Completer
Completer
<
String
?>?
_captureCompleter
;
UsbCameraService
.
_
()
:
_controller
=
UsbCameraController
();
/// 单例实例
static
UsbCameraService
get
instance
{
_instance
??=
UsbCameraService
.
_
();
return
_instance
!;
}
/// USB 设备事件流(设备插拔、摄像头状态、拍摄回调等)
Stream
<
CameraEvent
>
get
eventStream
=>
_eventController
.
stream
;
/// 当前控制器引用(供需要直接调用的场景)
UsbCameraController
get
controller
=>
_controller
;
/// 摄像头是否已打开
bool
_isOpened
=
false
;
bool
get
isOpened
=>
_isOpened
;
/// 是否正在录像
bool
_isRecording
=
false
;
bool
get
isRecording
=>
_isRecording
;
// ==================== 初始化与销毁 ====================
/// 初始化:注册 USB 监听,开始检测设备插拔
Future
<
void
>
initialize
()
async
{
if
(
_eventSub
!=
null
)
return
;
// 已初始化
// 监听插件事件流
_eventSub
=
UsbCameraController
.
events
.
listen
(
_handleEvent
);
// 注册 USB 监听
await
_controller
.
registerUsb
();
debugPrint
(
'[UsbCameraService] 初始化完成,USB 监听已注册'
);
}
/// 处理来自插件的所有事件
void
_handleEvent
(
CameraEvent
event
)
{
debugPrint
(
'[UsbCameraService] 收到事件:
${event.type.name}
, '
'deviceId=
${event.deviceId}
, state=
${event.state}
, path=
${event.path}
'
);
switch
(
event
.
type
)
{
case
CameraEventType
.
onAttachDev
:
// USB 摄像头插入 → 自动请求权限
if
(
event
.
deviceId
!=
null
)
{
_controller
.
requestPermission
(
event
.
deviceId
!);
}
break
;
case
CameraEventType
.
onDetachDev
:
case
CameraEventType
.
onDisConnectDev
:
// 设备拔出或断开
_isOpened
=
false
;
_isRecording
=
false
;
break
;
case
CameraEventType
.
onConnectDev
:
// 权限通过,自动打开摄像头
if
(
event
.
deviceId
!=
null
)
{
_openCameraInternal
(
event
.
deviceId
!);
}
break
;
case
CameraEventType
.
onCancelDev
:
// 权限被拒绝
_isOpened
=
false
;
break
;
case
CameraEventType
.
onCameraState
:
if
(
event
.
state
==
'opened'
)
{
_isOpened
=
true
;
}
else
if
(
event
.
state
==
'closed'
||
event
.
state
==
'error'
)
{
_isOpened
=
false
;
_isRecording
=
false
;
}
break
;
case
CameraEventType
.
onCaptureBegin
:
// 拍摄开始
break
;
case
CameraEventType
.
onCaptureComplete
:
// 拍摄完成 → 解析 Completer
_isRecording
=
false
;
if
(
_captureCompleter
!=
null
&&
!
_captureCompleter
!.
isCompleted
)
{
_captureCompleter
!.
complete
(
event
.
path
);
_captureCompleter
=
null
;
}
break
;
case
CameraEventType
.
onCaptureError
:
// 拍摄出错
_isRecording
=
false
;
if
(
_captureCompleter
!=
null
&&
!
_captureCompleter
!.
isCompleted
)
{
_captureCompleter
!.
completeError
(
Exception
(
event
.
error
??
'拍摄失败'
),
);
_captureCompleter
=
null
;
}
break
;
default
:
break
;
}
// 转发事件给所有订阅者
_eventController
.
add
(
event
);
}
/// 内部打开摄像头方法
Future
<
void
>
_openCameraInternal
(
int
deviceId
)
async
{
try
{
await
_controller
.
openCamera
(
deviceId
,
request:
const
CameraRequest
(
previewWidth:
1280
,
previewHeight:
720
,
renderMode:
RenderMode
.
opengl
,
previewFormat:
PreviewFormat
.
mjpeg
,
),
);
debugPrint
(
'[UsbCameraService] 摄像头已打开: deviceId=
$deviceId
'
);
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] 打开摄像头失败:
$e
'
);
}
}
// ==================== 设备管理 ====================
/// 获取已连接的 USB 设备列表
Future
<
List
<
UsbDevice
>>
getDeviceList
()
async
{
try
{
return
await
_controller
.
getDeviceList
();
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] getDeviceList error:
$e
'
);
return
[];
}
}
/// 手动打开指定摄像头
Future
<
bool
>
openCamera
(
int
deviceId
,
{
CameraRequest
?
request
})
async
{
try
{
await
_controller
.
openCamera
(
deviceId
,
request:
request
??
const
CameraRequest
(
previewWidth:
1280
,
previewHeight:
720
,
renderMode:
RenderMode
.
opengl
,
previewFormat:
PreviewFormat
.
mjpeg
,
),
);
return
true
;
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] openCamera error:
$e
'
);
return
false
;
}
}
// ==================== 摄像头控制 ====================
/// 关闭摄像头
Future
<
void
>
closeCamera
()
async
{
try
{
await
_controller
.
closeCamera
();
_isOpened
=
false
;
_isRecording
=
false
;
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] closeCamera error:
$e
'
);
}
}
/// 拍照
/// [customPath] 可选,指定照片保存路径;不传则自动生成
/// 返回照片文件路径
Future
<
String
?>
takePhoto
({
String
?
customPath
})
async
{
if
(!
_isOpened
)
{
debugPrint
(
'[UsbCameraService] 摄像头未打开,无法拍照'
);
return
null
;
}
try
{
_captureCompleter
=
Completer
<
String
?>();
final
path
=
customPath
??
await
_generatePhotoPath
();
final
success
=
await
_controller
.
captureImage
(
path:
path
);
if
(!
success
)
{
_captureCompleter
=
null
;
return
null
;
}
// 等待 onCaptureComplete 事件返回路径
final
result
=
await
_captureCompleter
!.
future
;
return
result
;
}
catch
(
e
)
{
_captureCompleter
=
null
;
debugPrint
(
'[UsbCameraService] takePhoto error:
$e
'
);
return
null
;
}
}
/// 开始录像
/// [customPath] 可选,指定视频保存路径
Future
<
bool
>
startRecord
({
String
?
customPath
})
async
{
if
(!
_isOpened
)
{
debugPrint
(
'[UsbCameraService] 摄像头未打开,无法录像'
);
return
false
;
}
try
{
final
path
=
customPath
??
await
_generateVideoPath
();
final
success
=
await
_controller
.
captureVideoStart
(
path:
path
);
if
(
success
)
{
_isRecording
=
true
;
}
return
success
;
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] startRecord error:
$e
'
);
return
false
;
}
}
/// 停止录像
/// 返回视频文件路径
Future
<
String
?>
stopRecord
()
async
{
if
(!
_isRecording
)
{
debugPrint
(
'[UsbCameraService] 当前未在录像'
);
return
null
;
}
try
{
_captureCompleter
=
Completer
<
String
?>();
final
success
=
await
_controller
.
captureVideoStop
();
if
(!
success
)
{
_captureCompleter
=
null
;
_isRecording
=
false
;
return
null
;
}
// 等待 onCaptureComplete 事件返回路径
final
result
=
await
_captureCompleter
!.
future
;
_isRecording
=
false
;
return
result
;
}
catch
(
e
)
{
_captureCompleter
=
null
;
_isRecording
=
false
;
debugPrint
(
'[UsbCameraService] stopRecord error:
$e
'
);
return
null
;
}
}
// ==================== 路径生成 ====================
/// 生成照片保存路径
Future
<
String
>
_generatePhotoPath
()
async
{
final
dir
=
await
_getCameraDirectory
();
final
timestamp
=
_timestamp
();
return
'
${dir.path}
/IMG_
$timestamp
.jpg'
;
}
/// 生成视频保存路径
Future
<
String
>
_generateVideoPath
()
async
{
final
dir
=
await
_getCameraDirectory
();
final
timestamp
=
_timestamp
();
return
'
${dir.path}
/VID_
$timestamp
.mp4'
;
}
/// 获取 USB 摄像头存储目录
Future
<
Directory
>
_getCameraDirectory
()
async
{
final
baseDir
=
Platform
.
isAndroid
?
await
getExternalStorageDirectory
()
:
await
getApplicationDocumentsDirectory
();
final
cameraDir
=
Directory
(
'
${baseDir!.path}
/usb_camera'
);
if
(!
await
cameraDir
.
exists
())
{
await
cameraDir
.
create
(
recursive:
true
);
}
return
cameraDir
;
}
/// 生成时间戳字符串
String
_timestamp
()
{
final
now
=
DateTime
.
now
();
return
'
${now.year}${_pad(now.month)}${_pad(now.day)}
_'
'
${_pad(now.hour)}${_pad(now.minute)}${_pad(now.second)}
'
;
}
String
_pad
(
int
value
)
=>
value
.
toString
().
padLeft
(
2
,
'0'
);
// ==================== 销毁 ====================
/// 释放所有资源
Future
<
void
>
dispose
()
async
{
_captureCompleter
=
null
;
_isOpened
=
false
;
_isRecording
=
false
;
await
_eventSub
?.
cancel
();
_eventSub
=
null
;
await
_eventController
.
close
();
await
_controller
.
dispose
();
debugPrint
(
'[UsbCameraService] 资源已释放'
);
}
}
lib/views/home/index/cubit/home_index_cubit.dart
deleted
100644 → 0
View file @
f0145cb5
import
'dart:async'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
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
'home_index_state.dart'
;
class
HomeIndexCubit
extends
Cubit
<
HomeIndexState
>
{
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,
/// 防止旧连接的异步结果污染当前模式的状态。
int
_switchGen
=
0
;
HomeIndexCubit
()
:
_monitoringService
=
MonitoringService
(),
_webrtcService
=
WebrtcService
(),
_p2pVideoService
=
P2pVideoService
(),
_storageService
=
StorageService
(),
_bluetoothManager
=
BleBluetoothManager
(),
_mcuReportDecoder
=
McuReportFrameDecoder
(),
super
(
const
HomeIndexState
())
{
_listenWebrtcState
();
_listenP2pState
();
_listenBluetoothState
();
_loadBoundBluetoothDevice
();
loadData
();
}
/// 监听 WebRTC 连接状态变化并同步到 State
void
_listenWebrtcState
()
{
_webrtcStateSub
=
_webrtcService
.
connectionStateStream
.
listen
((
state
)
{
if
(
isClosed
)
return
;
emit
(
this
.
state
.
copyWith
(
videoConnectionState:
state
));
});
}
/// 监听 P2P 视频服务状态变化并同步到 State
void
_listenP2pState
()
{
_p2pStateSub
=
_p2pVideoService
.
stateStream
.
listen
((
state
)
{
if
(
isClosed
)
return
;
emit
(
this
.
state
.
copyWith
(
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
(
isLoading:
true
,
error:
null
));
try
{
final
metrics
=
await
_monitoringService
.
getMetrics
();
if
(
isClosed
)
return
;
final
patientInfo
=
await
_monitoringService
.
getPatientInfo
();
if
(
isClosed
)
return
;
final
alerts
=
await
_monitoringService
.
getAlerts
();
if
(
isClosed
)
return
;
final
menuItems
=
await
_monitoringService
.
getMenuItems
();
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
isLoading:
false
,
status:
HomeIndexStatus
.
success
,
metrics:
metrics
,
patientInfo:
patientInfo
,
alerts:
alerts
,
menuItems:
menuItems
,
error:
null
,
));
// 数据加载完成后,按当前模式初始化视频连接
_initVideoByMode
();
}
catch
(
e
)
{
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
isLoading:
false
,
status:
HomeIndexStatus
.
failure
,
error:
e
.
toString
(),
));
}
}
/// 依据当前 [VideoStreamMode] 启动对应的视频连接
void
_initVideoByMode
()
{
if
(
state
.
videoStreamMode
==
VideoStreamMode
.
p2p
)
{
_initP2p
();
}
else
{
_initWebrtc
();
}
}
/// 初始化 WebRTC 视频连接(已有逻辑保持不变)
Future
<
void
>
_initWebrtc
()
async
{
await
_webrtcService
.
connect
(
// deviceNo: _mockDeviceNo,
// password: _mockDevicePassword,
deviceNo:
'VE10085871QOXG'
,
password:
'143548'
,
);
}
/// 手动重试 WebRTC
Future
<
void
>
retryWebrtc
()
async
{
await
_initWebrtc
();
}
/// 初始化 P2P 视频连接
Future
<
void
>
_initP2p
()
async
{
await
_p2pVideoService
.
connect
(
deviceId:
'VE10085871QOXG'
,
username:
'admin'
,
password:
'143548'
,
);
}
// ==================== 模式切换 ====================
/// 切换视频传输模式
///
/// 先停止当前模式 → 更新状态 → 启动新模式。
/// 使用 [_switchGen] 代际计数器防止旧连接的异步结果污染当前状态。
Future
<
void
>
switchStreamMode
(
VideoStreamMode
mode
)
async
{
if
(
state
.
isSwitchingMode
)
return
;
if
(
state
.
videoStreamMode
==
mode
)
return
;
final
currentGen
=
++
_switchGen
;
debugPrint
(
'[HomeIndexCubit] 切换模式:
${state.videoStreamMode}
→
$mode
(gen=
$currentGen
)'
);
// 1. 标记切换中
emit
(
state
.
copyWith
(
isSwitchingMode:
true
));
// 2. 停止当前模式(用 stop 停连接,保留 StreamController 可复用)
if
(
state
.
videoStreamMode
==
VideoStreamMode
.
p2p
)
{
await
_p2pVideoService
.
stop
();
}
else
{
await
_webrtcService
.
stop
();
}
// 3. 竞态检查:如果在清理期间又触发了一次切换则放弃本次
if
(
currentGen
!=
_switchGen
||
isClosed
)
return
;
// 4. 切换到新模式
emit
(
state
.
copyWith
(
videoStreamMode:
mode
,
isSwitchingMode:
false
,
isP2pConnected:
false
,
videoConnectionState:
WebrtcConnectionState
.
disconnected
,
));
// 5. 启动新模式视频连接
if
(
mode
==
VideoStreamMode
.
p2p
)
{
await
_initP2p
();
}
else
{
await
_initWebrtc
();
}
}
/// 手动重试当前模式的视频连接
Future
<
void
>
retryCurrentMode
()
async
{
if
(
state
.
videoStreamMode
==
VideoStreamMode
.
p2p
)
{
await
_p2pVideoService
.
stop
();
await
_initP2p
();
}
else
{
await
_webrtcService
.
stop
();
await
_initWebrtc
();
}
}
Future
<
void
>
refreshData
()
async
{
try
{
final
metrics
=
await
_monitoringService
.
getMetrics
();
if
(
isClosed
)
return
;
final
patientInfo
=
await
_monitoringService
.
getPatientInfo
();
if
(
isClosed
)
return
;
final
alerts
=
await
_monitoringService
.
getAlerts
();
if
(
isClosed
)
return
;
final
menuItems
=
await
_monitoringService
.
getMenuItems
();
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
status:
HomeIndexStatus
.
success
,
metrics:
metrics
,
patientInfo:
patientInfo
,
alerts:
alerts
,
menuItems:
menuItems
,
error:
null
,
));
}
catch
(
e
)
{
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
status:
HomeIndexStatus
.
failure
,
error:
e
.
toString
(),
));
}
}
// ==================== 蓝牙扫描/绑定 ====================
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
;
/// 获取 P2P 视频播放控制器供 UI 层使用
P2pVideoService
get
p2pVideoService
=>
_p2pVideoService
;
@override
Future
<
void
>
close
()
{
_webrtcStateSub
?.
cancel
();
_p2pStateSub
?.
cancel
();
_bluetoothScanSub
?.
cancel
();
_bluetoothStateSub
?.
cancel
();
_bluetoothMessageSub
?.
cancel
();
_bluetoothDataSub
?.
cancel
();
_webrtcService
.
dispose
();
_p2pVideoService
.
dispose
();
return
_bluetoothManager
.
release
().
then
((
_
)
=>
super
.
close
());
}
}
lib/views/home/index/cubit/home_index_state.dart
deleted
100644 → 0
View file @
f0145cb5
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
HomeIndexStatus
{
initial
,
loading
,
success
,
failure
,
}
class
HomeIndexState
extends
Equatable
{
final
HomeIndexStatus
status
;
final
bool
isLoading
;
final
String
?
error
;
final
List
<
MonitoringMetricBO
>
metrics
;
final
PatientInfoBO
?
patientInfo
;
final
List
<
AlertInfoBO
>
alerts
;
final
List
<
MonitoringMenuItemBO
>
menuItems
;
/// WebRTC 视频连接状态
final
WebrtcConnectionState
videoConnectionState
;
/// 当前视频传输模式
final
VideoStreamMode
videoStreamMode
;
/// 是否正在切换传输模式(用于 UI loading 状态)
final
bool
isSwitchingMode
;
/// 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
HomeIndexState
({
this
.
status
=
HomeIndexStatus
.
initial
,
this
.
isLoading
=
false
,
this
.
error
,
this
.
metrics
=
const
[],
this
.
patientInfo
,
this
.
alerts
=
const
[],
this
.
menuItems
=
const
[],
this
.
videoConnectionState
=
WebrtcConnectionState
.
disconnected
,
this
.
videoStreamMode
=
VideoStreamMode
.
webrtc
,
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
,
});
HomeIndexState
copyWith
({
HomeIndexStatus
?
status
,
bool
?
isLoading
,
String
?
error
,
List
<
MonitoringMetricBO
>?
metrics
,
PatientInfoBO
?
patientInfo
,
List
<
AlertInfoBO
>?
alerts
,
List
<
MonitoringMenuItemBO
>?
menuItems
,
WebrtcConnectionState
?
videoConnectionState
,
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
HomeIndexState
(
status:
status
??
this
.
status
,
isLoading:
isLoading
??
this
.
isLoading
,
error:
error
??
this
.
error
,
metrics:
metrics
??
this
.
metrics
,
patientInfo:
patientInfo
??
this
.
patientInfo
,
alerts:
alerts
??
this
.
alerts
,
menuItems:
menuItems
??
this
.
menuItems
,
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
,
);
}
@override
List
<
Object
?>
get
props
=>
[
status
,
isLoading
,
error
,
metrics
,
patientInfo
,
alerts
,
menuItems
,
videoConnectionState
,
videoStreamMode
,
isSwitchingMode
,
isP2pConnected
,
isBluetoothScanning
,
isBluetoothBinding
,
bindingBluetoothDeviceId
,
bluetoothDevices
,
boundBluetoothDeviceId
,
boundBluetoothDeviceName
,
bluetoothConnectionStatus
,
bluetoothMessage
,
latestBluetoothRawHex
,
latestMcuReport
,
];
}
lib/views/home/index/home_index_view.dart
deleted
100644 → 0
View file @
f0145cb5
import
'package:auto_route/auto_route.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:laki_icu_app/enums/video_stream_mode_enum.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.dart'
;
import
'cubit/home_index_cubit.dart'
;
import
'cubit/home_index_state.dart'
;
import
'widgets/alert_info_card.dart'
;
import
'widgets/bluetooth_bind_dialog.dart'
;
import
'widgets/metric_card.dart'
;
import
'widgets/patient_info_card.dart'
;
import
'widgets/sidebar_menu_item.dart'
;
import
'widgets/video_player_widget.dart'
;
@RoutePage
()
class
HomeIndexView
extends
StatelessWidget
{
const
HomeIndexView
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
(
create:
(
_
)
=>
HomeIndexCubit
(),
child:
const
HomeIndexContent
(),
);
}
}
class
HomeIndexContent
extends
StatefulWidget
{
const
HomeIndexContent
({
super
.
key
});
@override
State
<
HomeIndexContent
>
createState
()
=>
_HomeIndexContentState
();
}
class
_HomeIndexContentState
extends
State
<
HomeIndexContent
>
{
late
final
HomeIndexCubit
_homeIndexCubit
;
@override
void
initState
()
{
super
.
initState
();
_homeIndexCubit
=
context
.
read
<
HomeIndexCubit
>();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
backgroundColor:
const
Color
(
0xFF0F1F47
),
body:
SafeArea
(
child:
Column
(
children:
[
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
buildWhen:
(
previous
,
current
)
=>
previous
.
boundBluetoothDeviceId
!=
current
.
boundBluetoothDeviceId
,
builder:
(
context
,
state
)
=>
_buildAppBar
(
state
),
),
Expanded
(
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
builder:
(
context
,
state
)
{
if
(
state
.
isLoading
&&
state
.
metrics
.
isEmpty
)
{
return
const
Center
(
child:
CircularProgressIndicator
(
color:
Colors
.
white
),
);
}
if
(
state
.
status
==
HomeIndexStatus
.
failure
&&
state
.
metrics
.
isEmpty
)
{
return
_buildError
(
state
.
error
);
}
return
_buildMainContent
(
state
);
},
),
),
_buildFooter
(),
],
),
),
);
}
Widget
_buildError
(
String
?
error
)
{
return
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
children:
[
Text
(
'加载失败:
${error ?? '未知错误'}
'
,
style:
TextStyle
(
color:
Colors
.
white
,
fontSize:
24
.
sp
),
),
SizedBox
(
height:
24
.
h
),
ElevatedButton
(
onPressed:
()
=>
_homeIndexCubit
.
loadData
(),
child:
const
Text
(
'重试'
),
),
],
),
);
}
Widget
_buildAppBar
(
HomeIndexState
state
)
{
final
isBluetoothBound
=
state
.
boundBluetoothDeviceId
?.
isNotEmpty
==
true
;
return
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
24
.
w
,
vertical:
12
.
h
),
decoration:
const
BoxDecoration
(
gradient:
LinearGradient
(
colors:
[
Color
(
0xFF1E10B6
),
Color
(
0xFF3A87FF
)],
),
border:
Border
(
bottom:
BorderSide
(
color:
Colors
.
white24
,
width:
1
)),
),
child:
Row
(
children:
[
const
CircleAvatar
(
backgroundColor:
Colors
.
white
,
radius:
24
,
child:
Icon
(
Icons
.
local_hospital
,
color:
Color
(
0xFF1E10B6
),
size:
28
),
),
SizedBox
(
width:
16
.
w
),
Text
(
'加邦TSAAS动物医疗监护舱'
,
style:
TextStyle
(
fontSize:
28
.
sp
,
fontWeight:
FontWeight
.
bold
,
color:
Colors
.
white
,
),
),
const
Spacer
(),
_buildAppBarIcon
(
Icons
.
bluetooth
,
'TSAAS'
,
isActive:
isBluetoothBound
,
onTap:
_showBluetoothDialog
,
),
SizedBox
(
width:
16
.
w
),
_buildAppBarIcon
(
Icons
.
description
,
null
),
SizedBox
(
width:
16
.
w
),
_buildAppBarIcon
(
Icons
.
settings
,
null
),
],
),
);
}
Widget
_buildAppBarIcon
(
IconData
icon
,
String
?
label
,
{
bool
isActive
=
false
,
VoidCallback
?
onTap
,
})
{
final
foregroundColor
=
isActive
?
const
Color
(
0xFF003A8C
)
:
Colors
.
white
;
final
backgroundColor
=
isActive
?
const
Color
(
0xFF00F6FF
)
:
Colors
.
transparent
;
final
borderColor
=
isActive
?
const
Color
(
0xFFB8FFFF
)
:
Colors
.
white24
;
return
GestureDetector
(
onTap:
onTap
,
child:
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
12
.
w
,
vertical:
8
.
h
),
decoration:
BoxDecoration
(
color:
backgroundColor
,
border:
Border
.
all
(
color:
borderColor
,
width:
1
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
),
child:
Row
(
children:
[
Icon
(
icon
,
color:
foregroundColor
,
size:
20
.
w
),
if
(
label
!=
null
)
...[
SizedBox
(
width:
6
.
w
),
Text
(
label
,
style:
TextStyle
(
fontSize:
18
.
sp
,
color:
foregroundColor
,
fontWeight:
isActive
?
FontWeight
.
w700
:
FontWeight
.
w400
,
),
),
],
],
),
),
);
}
void
_showBluetoothDialog
()
{
showDialog
<
void
>(
context:
context
,
builder:
(
_
)
=>
BlocProvider
.
value
(
value:
_homeIndexCubit
,
child:
const
BluetoothBindDialog
(),
),
);
}
Widget
_buildMainContent
(
HomeIndexState
state
)
{
return
SingleChildScrollView
(
padding:
EdgeInsets
.
all
(
24
.
w
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// 左侧区域 (3/4宽度)
Expanded
(
flex:
3
,
child:
Column
(
children:
[
// 指标栏(100%宽度)
_buildMetricsSection
(
state
.
metrics
),
SizedBox
(
height:
24
.
h
),
// 下方:监控画面 + 右侧信息卡片
SizedBox
(
height:
500
.
h
,
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// 监控画面模块(2/3宽度、100%高度)
Expanded
(
flex:
2
,
child:
VideoPlayerWidget
(
streamMode:
state
.
videoStreamMode
,
isSwitchingMode:
state
.
isSwitchingMode
,
webrtcRenderer:
_homeIndexCubit
.
webrtcService
.
renderer
,
webrtcConnectionState:
state
.
videoConnectionState
,
p2pController:
_homeIndexCubit
.
p2pVideoService
.
playerController
,
isP2pConnected:
state
.
isP2pConnected
,
onRetry:
()
=>
_homeIndexCubit
.
retryCurrentMode
(),
onToggleMode:
_onToggleStreamMode
,
),
),
SizedBox
(
width:
24
.
w
),
// 右侧(1/3宽度):患者信息卡片 + 告警信息卡片
Expanded
(
flex:
1
,
child:
Column
(
children:
[
// 患者信息卡片(50%高度)
Expanded
(
flex:
1
,
child:
state
.
patientInfo
==
null
?
_buildEmptyCard
(
'暂无患者信息'
)
:
PatientInfoCard
(
patientInfo:
state
.
patientInfo
!,
),
),
SizedBox
(
height:
24
.
h
),
// 告警信息卡片(50%高度)
Expanded
(
flex:
1
,
child:
AlertInfoCard
(
alerts:
state
.
alerts
),
),
],
),
),
],
),
),
],
),
),
SizedBox
(
width:
24
.
w
),
// 右侧区域 (1/4宽度)
Expanded
(
flex:
1
,
child:
SizedBox
(
height:
500
.
h
,
child:
Column
(
children:
state
.
menuItems
.
map
((
item
)
{
return
Expanded
(
child:
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
24
.
h
),
child:
SidebarMenuItem
(
item:
item
,
onTap:
()
{},
),
),
);
}).
toList
(),
),
),
),
],
),
);
}
/// 模式切换回调
void
_onToggleStreamMode
()
{
final
cubit
=
_homeIndexCubit
;
final
currentMode
=
cubit
.
state
.
videoStreamMode
;
final
nextMode
=
currentMode
==
VideoStreamMode
.
p2p
?
VideoStreamMode
.
webrtc
:
VideoStreamMode
.
p2p
;
cubit
.
switchStreamMode
(
nextMode
);
}
Widget
_buildEmptyCard
(
String
text
)
{
return
Container
(
padding:
EdgeInsets
.
all
(
24
.
w
),
decoration:
BoxDecoration
(
gradient:
const
LinearGradient
(
colors:
[
Color
(
0xFF1E10B6
),
Color
(
0xFF3A87FF
)],
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
1
),
),
child:
Center
(
child:
Text
(
text
,
style:
TextStyle
(
color:
Colors
.
white70
,
fontSize:
24
.
sp
),
),
),
);
}
Widget
_buildMetricsSection
(
List
<
MonitoringMetricBO
>
metrics
)
{
return
Container
(
padding:
EdgeInsets
.
all
(
24
.
w
),
decoration:
BoxDecoration
(
gradient:
const
LinearGradient
(
colors:
[
Color
(
0xFF1E10B6
),
Color
(
0xFF3A87FF
)],
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
1
),
),
child:
Row
(
children:
metrics
.
map
((
metric
)
{
return
Expanded
(
child:
Padding
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
8
.
w
),
child:
MetricCard
(
metric:
metric
),
),
);
}).
toList
(),
),
);
}
Widget
_buildFooter
()
{
return
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
24
.
w
,
vertical:
12
.
h
),
decoration:
const
BoxDecoration
(
gradient:
LinearGradient
(
colors:
[
Color
(
0xFF1E10B6
),
Color
(
0xFF3A87FF
)],
),
border:
Border
(
top:
BorderSide
(
color:
Colors
.
white24
,
width:
1
)),
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
Text
(
'总运行时长: 35 h'
,
style:
TextStyle
(
fontSize:
20
.
sp
,
color:
Colors
.
white70
,
),
),
Text
(
'环境温度: 25.6 ℃'
,
style:
TextStyle
(
fontSize:
20
.
sp
,
color:
Colors
.
white70
,
),
),
],
),
);
}
}
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
deleted
100644 → 0
View file @
f0145cb5
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/home/index/cubit/home_index_cubit.dart'
;
import
'package:laki_icu_app/views/home/index/cubit/home_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
<
HomeIndexCubit
>();
if
(!
cubit
.
state
.
isBluetoothScanning
)
{
cubit
.
scanBluetoothDevices
();
}
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
Dialog
(
backgroundColor:
const
Color
(
0xFFF4F4F4
),
insetPadding:
EdgeInsets
.
zero
,
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
builder:
(
context
,
state
)
{
return
SizedBox
.
expand
(
child:
_buildContent
(
context
,
state
),
);
},
),
);
}
Widget
_buildContent
(
BuildContext
context
,
HomeIndexState
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
,
HomeIndexState
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
,
HomeIndexState
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
<
HomeIndexCubit
>().
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
,
HomeIndexState
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
<
HomeIndexCubit
>().
bindBluetoothDevice
(
device
,
),
);
},
);
}
List
<
BluetoothScanDevice
>
_sortDevicesForDisplay
(
HomeIndexState
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
<
HomeIndexCubit
>().
unbindBluetoothDevice
();
}
}
Widget
_buildLatestData
(
HomeIndexState
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
,
),
),
),
],
),
),
);
}
}
pubspec.yaml
View file @
ddc45594
...
@@ -50,8 +50,8 @@ dependencies:
...
@@ -50,8 +50,8 @@ dependencies:
logger
:
^2.7.0
logger
:
^2.7.0
fluttertoast
:
^9.0.0
fluttertoast
:
^9.0.0
permission_handler
:
^11.3.1
permission_handler
:
^11.3.1
flutter_usbcamera
:
#
flutter_usbcamera:
path
:
./third_party/flutter_usbcamera
#
path: ./third_party/flutter_usbcamera
path_provider
:
^2.1.2
path_provider
:
^2.1.2
flutter_webrtc
:
flutter_webrtc
:
path
:
./plugins/flutter_webrtc
path
:
./plugins/flutter_webrtc
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment