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
cf1809a5
Commit
cf1809a5
authored
Jun 17, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
usb摄像头对接完成
parent
026b37a8
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
257 additions
and
20 deletions
+257
-20
tcp_client_cubit.dart
lib/utils/tcp/service/tcp_client_cubit.dart
+27
-4
tcp_server_cubit.dart
lib/utils/tcp/service/tcp_server_cubit.dart
+34
-4
home_index_cubit.dart
lib/views/home/index/cubit/home_index_cubit.dart
+10
-0
camera_controls.dart
lib/views/home/index/widgets/camera_controls.dart
+14
-1
monitoring_screen.dart
lib/views/home/index/widgets/monitoring_screen.dart
+159
-10
login_cubit.dart
lib/views/login/cubit/login_cubit.dart
+13
-1
No files found.
lib/utils/tcp/service/tcp_client_cubit.dart
View file @
cf1809a5
...
...
@@ -20,6 +20,9 @@ class TcpClientCubit extends Cubit<TcpClientState> {
bool
_isManualDisconnect
=
false
;
bool
_isConnecting
=
false
;
/// Socket 数据监听的订阅,需要在 close 时显式取消
StreamSubscription
<
dynamic
>?
_socketSub
;
final
StreamController
<
Uint8List
>
_dataController
=
StreamController
.
broadcast
();
...
...
@@ -65,6 +68,12 @@ class TcpClientCubit extends Cubit<TcpClientState> {
timeout:
Duration
(
milliseconds:
_timeout
),
);
if
(
isClosed
)
{
_socket
?.
close
();
_socket
=
null
;
return
false
;
}
_socket
!.
setOption
(
SocketOption
.
tcpNoDelay
,
true
);
_listenSocket
();
...
...
@@ -73,6 +82,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_startHeartbeat
();
return
true
;
}
catch
(
e
)
{
if
(
isClosed
)
return
false
;
emit
(
state
.
copyWith
(
connectionStatus:
TcpConnectionStatus
.
error
,
error:
e
.
toString
(),
...
...
@@ -86,7 +96,8 @@ class TcpClientCubit extends Cubit<TcpClientState> {
/// 监听 Socket 数据
void
_listenSocket
()
{
_socket
!.
listen
(
_socketSub
?.
cancel
();
_socketSub
=
_socket
!.
listen
(
(
data
)
{
final
uint8List
=
Uint8List
.
fromList
(
data
);
if
(!
_dataController
.
isClosed
)
{
...
...
@@ -94,6 +105,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
}
},
onError:
(
e
)
{
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
connectionStatus:
TcpConnectionStatus
.
error
,
error:
e
.
toString
(),
...
...
@@ -101,7 +113,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_tryReconnect
();
},
onDone:
()
{
if
(!
_isManualDisconnect
)
{
if
(!
_isManualDisconnect
&&
!
isClosed
)
{
_tryReconnect
();
}
},
...
...
@@ -110,6 +122,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
/// 自动重连
void
_tryReconnect
()
{
if
(
isClosed
)
return
;
if
(
_isManualDisconnect
)
return
;
if
(
_currentRetry
>=
_maxRetryCount
)
{
emit
(
state
.
copyWith
(
connectionStatus:
TcpConnectionStatus
.
disconnected
));
...
...
@@ -121,7 +134,7 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_reconnectTimer
?.
cancel
();
_reconnectTimer
=
Timer
(
Duration
(
milliseconds:
_retryInterval
),
()
{
connect
();
if
(!
isClosed
)
connect
();
});
}
...
...
@@ -173,14 +186,24 @@ class TcpClientCubit extends Cubit<TcpClientState> {
_isManualDisconnect
=
true
;
_reconnectTimer
?.
cancel
();
_heartbeatTimer
?.
cancel
();
_socketSub
?.
cancel
();
_socketSub
=
null
;
_socket
?.
close
();
_socket
=
null
;
if
(!
isClosed
)
{
emit
(
state
.
copyWith
(
connectionStatus:
TcpConnectionStatus
.
disconnected
));
}
}
@override
Future
<
void
>
close
()
{
disconnect
();
_isManualDisconnect
=
true
;
_reconnectTimer
?.
cancel
();
_heartbeatTimer
?.
cancel
();
_socketSub
?.
cancel
();
_socketSub
=
null
;
_socket
?.
close
();
_socket
=
null
;
_dataController
.
close
();
return
super
.
close
();
}
...
...
lib/utils/tcp/service/tcp_server_cubit.dart
View file @
cf1809a5
...
...
@@ -8,6 +8,12 @@ class TcpServerCubit extends Cubit<TcpServerState> {
ServerSocket
?
_serverSocket
;
final
List
<
Socket
>
_clients
=
[];
/// 服务端 Socket 监听的订阅
StreamSubscription
<
Socket
>?
_serverSub
;
/// 每个客户端 Socket 的订阅
final
List
<
StreamSubscription
<
dynamic
>>
_clientSubs
=
[];
final
StreamController
<
Map
<
String
,
dynamic
>>
_dataController
=
StreamController
.
broadcast
();
...
...
@@ -26,9 +32,15 @@ class TcpServerCubit extends Cubit<TcpServerState> {
try
{
_serverSocket
=
await
ServerSocket
.
bind
(
InternetAddress
.
anyIPv4
,
state
.
port
);
if
(
isClosed
)
{
_serverSocket
?.
close
();
_serverSocket
=
null
;
return
;
}
emit
(
state
.
copyWith
(
isRunning:
true
,
port:
_serverSocket
!.
port
));
_listenClients
();
}
catch
(
e
)
{
if
(
isClosed
)
return
;
if
(
e
.
toString
().
contains
(
'already in use'
))
{
emit
(
state
.
copyWith
(
port:
state
.
port
+
1
));
start
();
...
...
@@ -40,7 +52,12 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 监听客户端连接
void
_listenClients
()
{
_serverSocket
!.
listen
((
client
)
{
_serverSub
?.
cancel
();
_serverSub
=
_serverSocket
!.
listen
((
client
)
{
if
(
isClosed
)
{
client
.
close
();
return
;
}
_clients
.
add
(
client
);
emit
(
state
.
copyWith
(
connectedClients:
_clients
.
length
));
_handleClient
(
client
);
...
...
@@ -49,8 +66,9 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 处理单个客户端数据
void
_handleClient
(
Socket
client
)
{
client
.
listen
(
final
sub
=
client
.
listen
(
(
data
)
{
if
(
_dataController
.
isClosed
)
return
;
_dataController
.
add
({
'client'
:
client
,
'address'
:
client
.
remoteAddress
.
address
,
...
...
@@ -61,6 +79,7 @@ class TcpServerCubit extends Cubit<TcpServerState> {
onDone:
()
=>
_removeClient
(
client
),
onError:
(
e
)
=>
_removeClient
(
client
),
);
_clientSubs
.
add
(
sub
);
}
/// 群发字节数据给所有客户端
...
...
@@ -85,24 +104,35 @@ class TcpServerCubit extends Cubit<TcpServerState> {
/// 移除客户端
void
_removeClient
(
Socket
client
)
{
_clients
.
remove
(
client
);
emit
(
state
.
copyWith
(
connectedClients:
_clients
.
length
));
client
.
destroy
();
if
(!
isClosed
)
{
emit
(
state
.
copyWith
(
connectedClients:
_clients
.
length
));
}
}
/// 停止服务端
Future
<
void
>
stop
()
async
{
for
(
var
sub
in
_clientSubs
)
{
await
sub
.
cancel
();
}
_clientSubs
.
clear
();
for
(
var
c
in
_clients
)
{
await
c
.
close
();
}
_clients
.
clear
();
await
_serverSocket
?.
close
();
_serverSocket
=
null
;
if
(!
isClosed
)
{
emit
(
state
.
copyWith
(
isRunning:
false
,
connectedClients:
0
));
}
}
@override
Future
<
void
>
close
()
async
{
await
stop
();
_dataController
.
close
();
_serverSub
?.
cancel
();
_serverSub
=
null
;
await
_dataController
.
close
();
return
super
.
close
();
}
}
lib/views/home/index/cubit/home_index_cubit.dart
View file @
cf1809a5
...
...
@@ -16,9 +16,13 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
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
,
...
...
@@ -30,6 +34,7 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
error:
null
,
));
}
catch
(
e
)
{
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
isLoading:
false
,
status:
HomeIndexStatus
.
failure
,
...
...
@@ -41,9 +46,13 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
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
,
...
...
@@ -54,6 +63,7 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
error:
null
,
));
}
catch
(
e
)
{
if
(
isClosed
)
return
;
emit
(
state
.
copyWith
(
status:
HomeIndexStatus
.
failure
,
error:
e
.
toString
(),
...
...
lib/views/home/index/widgets/camera_controls.dart
View file @
cf1809a5
...
...
@@ -17,6 +17,9 @@ class CameraControls extends StatelessWidget {
/// 摄像头是否已就绪(已打开)
final
bool
cameraReady
;
/// 是否已全屏
final
bool
isFullscreen
;
/// 拍照回调
final
VoidCallback
?
onTakePhoto
;
...
...
@@ -26,13 +29,18 @@ class CameraControls extends StatelessWidget {
/// 刷新回调
final
VoidCallback
?
onRefresh
;
/// 切换全屏回调
final
VoidCallback
?
onToggleFullscreen
;
const
CameraControls
({
super
.
key
,
this
.
isRecording
=
false
,
this
.
cameraReady
=
false
,
this
.
isFullscreen
=
false
,
this
.
onTakePhoto
,
this
.
onToggleRecord
,
this
.
onRefresh
,
this
.
onToggleFullscreen
,
});
@override
...
...
@@ -47,7 +55,12 @@ class CameraControls extends StatelessWidget {
enabled:
true
,
// 刷新按钮始终可用
),
SizedBox
(
width:
16
.
w
),
// todo 放大,全屏
_buildControlButton
(
icon:
isFullscreen
?
Icons
.
fullscreen_exit
:
Icons
.
fullscreen
,
onPressed:
onToggleFullscreen
,
tooltip:
isFullscreen
?
'退出全屏'
:
'全屏放大'
,
enabled:
cameraReady
,
),
// _buildControlButton(
// icon: Icons.camera_alt,
...
...
lib/views/home/index/widgets/monitoring_screen.dart
View file @
cf1809a5
...
...
@@ -12,7 +12,7 @@ import 'camera_controls.dart';
/// 职责:
/// - 通过 [UsbCameraPreview] 嵌入 USB 摄像头实时预览画面
/// - 监听 [UsbCameraService.eventStream] 处理设备热插拔和状态变化
/// - 底部控制栏:
拍照、录像、刷新
按钮
/// - 底部控制栏:
刷新、全屏
按钮
///
/// 涉及页面:首页监护舱页面
class
MonitoringScreen
extends
StatefulWidget
{
...
...
@@ -38,6 +38,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
bool
_cameraReady
=
false
;
bool
_isRecording
=
false
;
bool
_isFullscreen
=
false
;
/// 用于强制重建 PlatformView 的计数器
/// flutter_usbcamera 底层使用单例 [mCameraView] 绑定唯一 TextureView,
/// 退出全屏时 generation+1 使原始 UsbCameraPreview 以新 Key 重建,
/// 触发原生 onCameraViewCreated → mCameraView 重新指向此处的 TextureView
int
_cameraViewGeneration
=
0
;
OverlayEntry
?
_fullscreenEntry
;
@override
void
initState
()
{
...
...
@@ -128,21 +137,28 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
@override
void
dispose
()
{
_eventSub
?.
cancel
();
_fullscreenEntry
?.
remove
();
_fullscreenEntry
=
null
;
_cameraService
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
// AspectRatio 放在最外层,让整个 Container 宽度贴合 16:9 视频比例,
// 避免父级 Expanded 撑宽后出现右侧空白
return
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
child:
AspectRatio
(
aspectRatio:
16
/
9
,
child:
Container
(
decoration:
BoxDecoration
(
gradient:
const
LinearGradient
(
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
colors:
[
Color
(
0xCC63A0FF
),
Color
(
0xCC0024C4
)],
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
1
),
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
4
),
),
child:
Stack
(
children:
[
...
...
@@ -157,24 +173,28 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child:
CameraControls
(
isRecording:
_isRecording
,
cameraReady:
_cameraReady
,
isFullscreen:
_isFullscreen
,
onTakePhoto:
_onTakePhoto
,
onToggleRecord:
_onToggleRecord
,
onRefresh:
_onRefresh
,
onToggleFullscreen:
_onToggleFullscreen
,
),
),
],
),
),
),
);
}
/// 构建 USB 摄像头预览画面(通过 flutter_usbcamera 插件)
///
/// 使用 [_cameraViewGeneration] 作为 Key 的一部分:
/// 退出全屏时 generation+1 使 Widget 以新 Key 重建,
/// 对应的 PlatformView 也随之重建并触发 onCameraViewCreated
Widget
_buildCameraPreview
()
{
return
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
child:
const
AspectRatio
(
aspectRatio:
16
/
9
,
child:
UsbCameraPreview
(),
),
return
UsbCameraPreview
(
key:
ValueKey
(
'camera_
$_cameraViewGeneration
'
),
);
}
...
...
@@ -256,6 +276,86 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
widget
.
onRefresh
?.
call
();
}
// ==================== 全屏逻辑 ====================
/// 切换全屏模式
///
/// flutter_usbcamera 底层将 [mCameraView] 作为单例绑定唯一 TextureView,
/// 不能同时在两个 Widget 中渲染 [UsbCameraPreview](第二个会黑屏)。
///
/// 解决思路:关闭摄像头 → 切换 View 容器 → 重新打开摄像头。
/// - 进入全屏:关摄像头 → 插 Overlay(含 UsbCameraPreview)→ 开摄像头
/// - 退出全屏:关摄像头 → 移 Overlay → 增 generation 强制重建原始
/// PlatformView → 开摄像头
Future
<
void
>
_onToggleFullscreen
()
async
{
if
(!
_cameraReady
)
{
_showSnackBar
(
'摄像头未就绪'
);
return
;
}
if
(
_isFullscreen
)
{
await
_exitFullscreen
();
}
else
{
await
_enterFullscreen
();
}
}
/// 进入全屏
Future
<
void
>
_enterFullscreen
()
async
{
final
deviceId
=
_cameraService
.
controller
.
currentDeviceId
;
if
(
deviceId
==
null
)
return
;
// 在 await 之前捕获 overlay,避免 use_build_context_synchronously
final
overlay
=
Overlay
.
of
(
context
);
// 1. 关闭摄像头,释放当前 PlatformView 的绑定
await
_cameraService
.
closeCamera
();
// 2. 插入全屏 Overlay —— 其 UsbCameraPreview 会创建新 PlatformView
// 并触发原生 onCameraViewCreated → mCameraView 指向新 TextureView
setState
(()
=>
_isFullscreen
=
true
);
_fullscreenEntry
=
OverlayEntry
(
builder:
(
_
)
=>
_FullscreenOverlay
(
onExit:
_exitFullscreen
),
);
overlay
.
insert
(
_fullscreenEntry
!);
// 3. 等待 PlatformView 初始化完成
await
Future
.
delayed
(
const
Duration
(
milliseconds:
150
));
// 4. 重新打开摄像头,渲染到 Overlay 中的 TextureView
await
_cameraService
.
openCamera
(
deviceId
);
}
/// 退出全屏
Future
<
void
>
_exitFullscreen
()
async
{
final
deviceId
=
_cameraService
.
controller
.
currentDeviceId
;
// 1. 关闭摄像头
if
(
deviceId
!=
null
)
{
await
_cameraService
.
closeCamera
();
}
// 2. 移除全屏 Overlay(其 PlatformView 被销毁)
_fullscreenEntry
?.
remove
();
_fullscreenEntry
=
null
;
// 3. 增加 generation → rebuild 时 UsbCameraPreview 的 Key 变化
// → 旧 PlatformView 被销毁,新 PlatformView 被创建
// → onCameraViewCreated → mCameraView 指向此处的 TextureView
setState
(()
{
_isFullscreen
=
false
;
_cameraViewGeneration
++;
});
// 4. 等待新 PlatformView 初始化完成
await
Future
.
delayed
(
const
Duration
(
milliseconds:
150
));
// 5. 重新打开摄像头,渲染到原始位置的 TextureView
if
(
deviceId
!=
null
)
{
await
_cameraService
.
openCamera
(
deviceId
);
}
}
void
_showSnackBar
(
String
message
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
...
...
@@ -270,3 +370,52 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
);
}
}
/// 全屏摄像头预览浮层
///
/// 通过 [OverlayEntry] 插入到当前页面,而非通过 Navigator.push 创建新路由,
/// 避免 flutter_usbcamera 的 PlatformView 冲突。
class
_FullscreenOverlay
extends
StatelessWidget
{
final
VoidCallback
onExit
;
const
_FullscreenOverlay
({
required
this
.
onExit
});
@override
Widget
build
(
BuildContext
context
)
{
return
Material
(
color:
Colors
.
black
,
child:
Stack
(
children:
[
// 全屏摄像头画面(16:9 居中)
const
Center
(
child:
AspectRatio
(
aspectRatio:
16
/
9
,
child:
UsbCameraPreview
(),
),
),
// 顶部退出按钮
Positioned
(
top:
MediaQuery
.
of
(
context
).
padding
.
top
+
16
.
h
,
left:
16
.
w
,
child:
GestureDetector
(
onTap:
onExit
,
child:
Container
(
width:
48
.
w
,
height:
48
.
w
,
decoration:
BoxDecoration
(
color:
Colors
.
black38
,
borderRadius:
BorderRadius
.
circular
(
24
.
r
),
),
child:
Icon
(
Icons
.
fullscreen_exit
,
color:
Colors
.
white
,
size:
28
.
w
,
),
),
),
),
],
),
);
}
}
lib/views/login/cubit/login_cubit.dart
View file @
cf1809a5
import
'dart:async'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:laki_icu_app/blocs/auth/auth_bloc.dart'
;
import
'package:laki_icu_app/blocs/auth/auth_event.dart'
;
...
...
@@ -9,6 +11,8 @@ class LoginCubit extends Cubit<LoginState> {
final
AuthBloc
authBloc
;
final
StorageService
storageService
;
StreamSubscription
<
AuthState
>?
_authSub
;
LoginCubit
({
required
this
.
authBloc
,
required
this
.
storageService
,
...
...
@@ -17,7 +21,8 @@ class LoginCubit extends Cubit<LoginState> {
}
void
_listenAuthState
()
{
authBloc
.
stream
.
listen
((
authState
)
{
_authSub
=
authBloc
.
stream
.
listen
((
authState
)
{
if
(
isClosed
)
return
;
if
(
authState
is
AuthFailure
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
authState
.
error
));
}
else
if
(
authState
is
AuthLoading
)
{
...
...
@@ -54,6 +59,7 @@ class LoginCubit extends Cubit<LoginState> {
Future
<
void
>
loadRememberCredentials
()
async
{
final
credentials
=
await
storageService
.
getRememberCredentials
();
if
(
isClosed
)
return
;
if
(
credentials
[
'username'
]
!=
null
)
{
emit
(
state
.
copyWith
(
rememberPassword:
true
,
...
...
@@ -66,4 +72,10 @@ class LoginCubit extends Cubit<LoginState> {
void
forgotPwd
()
{
// TODO: 跳转到忘记密码页面
}
@override
Future
<
void
>
close
()
{
_authSub
?.
cancel
();
return
super
.
close
();
}
}
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