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
531a4d4e
Commit
531a4d4e
authored
Jun 21, 2026
by
akari
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 添加蓝牙,MQTT连接
parent
b56b4a34
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
722 additions
and
24 deletions
+722
-24
.gitignore
.gitignore
+4
-0
settings.json
.vscode/settings.json
+30
-1
analysis_options.yaml
analysis_options.yaml
+10
-0
AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
+8
-0
gradle.properties
android/gradle.properties
+5
-1
user_model.dart
lib/models/bo/user_model.dart
+3
-0
bluetooth_manager.dart
lib/utils/bluetooth/bluetooth_manager.dart
+0
-0
index.dart
lib/utils/bluetooth/index.dart
+2
-0
mcu_report.dart
lib/utils/bluetooth/mcu_report.dart
+0
-0
index.dart
lib/utils/mqtt/index.dart
+1
-0
laki_mqtt_client.dart
lib/utils/mqtt/laki_mqtt_client.dart
+325
-0
storage_service.dart
lib/utils/storage/storage_service.dart
+45
-0
home_index_cubit.dart
lib/views/home/index/cubit/home_index_cubit.dart
+152
-6
home_index_state.dart
lib/views/home/index/cubit/home_index_state.dart
+82
-2
home_index_view.dart
lib/views/home/index/home_index_view.dart
+48
-9
bluetooth_bind_dialog.dart
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
+0
-0
GeneratedPluginRegistrant.swift
macos/Flutter/GeneratedPluginRegistrant.swift
+2
-0
pubspec.yaml
pubspec.yaml
+5
-5
No files found.
.gitignore
View file @
531a4d4e
# FVM Version Cache
.fvm/
.DS_Store
# claude code
.claude/
...
...
@@ -9,6 +10,9 @@
.dart_tool/
build/
.flutter-plugins
.flutter-plugins-dependencies
third_party/
pubspec.lock
...
...
.vscode/settings.json
View file @
531a4d4e
{
"dart.flutterSdkPath"
:
".fvm/versions/3.41.9"
,
"java.configuration.updateBuildConfiguration"
:
"disabled"
"java.configuration.updateBuildConfiguration"
:
"disabled"
,
"files.exclude"
:
{
"**/.dart_tool"
:
true
,
"**/.gradle"
:
true
,
"**/.idea"
:
true
,
"**/build"
:
true
,
"**/third_party"
:
true
,
"**/*.g.dart"
:
true
,
"**/*.gr.dart"
:
true
,
"**/*.freezed.dart"
:
true
,
"**/*.mocks.dart"
:
true
},
"search.exclude"
:
{
"**/.dart_tool"
:
true
,
"**/.gradle"
:
true
,
"**/.idea"
:
true
,
"**/build"
:
true
,
"**/third_party"
:
true
,
"**/*.g.dart"
:
true
,
"**/*.gr.dart"
:
true
,
"**/*.freezed.dart"
:
true
,
"**/*.mocks.dart"
:
true
},
"files.watcherExclude"
:
{
"**/.dart_tool/**"
:
true
,
"**/.gradle/**"
:
true
,
"**/.idea/**"
:
true
,
"**/build/**"
:
true
,
"**/third_party/**"
:
true
}
}
analysis_options.yaml
View file @
531a4d4e
...
...
@@ -9,6 +9,16 @@
# packages, and plugins designed to encourage good coding practices.
include
:
package:flutter_lints/flutter.yaml
analyzer
:
exclude
:
-
"
**/*.g.dart"
-
"
**/*.gr.dart"
-
"
**/*.freezed.dart"
-
"
**/*.mocks.dart"
-
"
.dart_tool/**"
-
"
build/**"
-
"
third_party/**"
linter
:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
...
...
android/app/src/main/AndroidManifest.xml
View file @
531a4d4e
...
...
@@ -4,6 +4,14 @@
<uses-permission
android:name=
"android.permission.INTERNET"
/>
<uses-permission
android:name=
"android.permission.ACCESS_NETWORK_STATE"
/>
<!-- BLE 蓝牙扫描/连接权限 -->
<uses-feature
android:name=
"android.hardware.bluetooth_le"
android:required=
"false"
/>
<uses-permission
android:name=
"android.permission.BLUETOOTH"
android:maxSdkVersion=
"30"
/>
<uses-permission
android:name=
"android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion=
"30"
/>
<uses-permission
android:name=
"android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion=
"30"
/>
<uses-permission
android:name=
"android.permission.BLUETOOTH_SCAN"
/>
<uses-permission
android:name=
"android.permission.BLUETOOTH_CONNECT"
/>
<!-- USB 摄像头权限 -->
<uses-feature
android:name=
"android.hardware.usb.host"
android:required=
"true"
/>
<uses-permission
android:name=
"android.permission.CAMERA"
/>
...
...
android/gradle.properties
View file @
531a4d4e
org.gradle.java.home
=
/
Library/Java/JavaVirtualMachines/jdk-22.0.1.jdk
/Contents/Home
org.gradle.java.home
=
/
Applications/Android Studio.app/Contents/jbr
/Contents/Home
org.gradle.jvmargs
=
-Xmx4G
android.useAndroidX
=
true
android.enableJetifier
=
true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin
=
false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl
=
false
lib/models/bo/user_model.dart
View file @
531a4d4e
...
...
@@ -11,6 +11,9 @@ class UserModel extends Equatable {
final
dynamic
hospitalId
;
final
String
?
hospitalName
;
// ignore: non_constant_identifier_names
static
String
?
MAC
;
const
UserModel
({
required
this
.
id
,
this
.
nickname
,
...
...
lib/utils/bluetooth/bluetooth_manager.dart
0 → 100644
View file @
531a4d4e
This diff is collapsed.
Click to expand it.
lib/utils/bluetooth/index.dart
0 → 100644
View file @
531a4d4e
export
'bluetooth_manager.dart'
;
export
'mcu_report.dart'
;
lib/utils/bluetooth/mcu_report.dart
0 → 100644
View file @
531a4d4e
This diff is collapsed.
Click to expand it.
lib/utils/mqtt/index.dart
0 → 100644
View file @
531a4d4e
export
'laki_mqtt_client.dart'
;
lib/utils/mqtt/laki_mqtt_client.dart
0 → 100644
View file @
531a4d4e
import
'dart:async'
;
import
'dart:convert'
;
import
'dart:io'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
import
'package:mqtt_client/mqtt_client.dart'
;
import
'package:mqtt_client/mqtt_server_client.dart'
;
enum
LakiMqttConnectionState
{
disconnected
,
connecting
,
connected
,
failed
,
}
class
LakiMqttConfig
{
const
LakiMqttConfig
({
required
this
.
host
,
required
this
.
clientId
,
this
.
port
=
1883
,
this
.
username
,
this
.
password
,
this
.
useSsl
=
false
,
this
.
useWebSocket
=
false
,
this
.
webSocketProtocols
,
this
.
keepAliveSeconds
=
30
,
this
.
connectTimeoutMs
=
5000
,
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
;
final
String
host
;
final
int
port
;
final
String
clientId
;
final
String
?
username
;
final
String
?
password
;
final
bool
useSsl
;
final
bool
useWebSocket
;
final
List
<
String
>?
webSocketProtocols
;
final
int
keepAliveSeconds
;
final
int
connectTimeoutMs
;
final
bool
logging
;
bool
get
isConfigured
=>
host
.
isNotEmpty
&&
clientId
.
isNotEmpty
;
}
class
LakiMqttTopics
{
const
LakiMqttTopics
.
_
();
static
String
icuUp
(
String
deviceSn
)
=>
'icu/
$deviceSn
/up'
;
static
String
icuTo
(
String
deviceSn
)
=>
'icu/
$deviceSn
/to'
;
static
String
icuPetBind
(
String
deviceSn
)
=>
'icu/
$deviceSn
/pet/bind'
;
static
String
cameraTo
(
String
cabinSn
)
=>
'camera/
$cabinSn
/to'
;
static
const
String
clientConnected
=
r'$events/client_connected'
;
static
const
String
clientDisconnected
=
r'$events/client_disconnected'
;
static
const
String
messageDelivered
=
r'$events/message_delivered'
;
}
class
LakiMqttMessage
{
const
LakiMqttMessage
({
required
this
.
topic
,
required
this
.
payload
,
this
.
json
,
});
final
String
topic
;
final
String
payload
;
final
Map
<
String
,
dynamic
>?
json
;
}
class
LakiMqttClient
{
LakiMqttClient
({
required
LakiMqttConfig
config
})
:
_config
=
config
;
final
LakiMqttConfig
_config
;
MqttServerClient
?
_client
;
StreamSubscription
<
List
<
MqttReceivedMessage
<
MqttMessage
>>>?
_updatesSub
;
final
_stateController
=
StreamController
<
LakiMqttConnectionState
>.
broadcast
();
final
_messageController
=
StreamController
<
LakiMqttMessage
>.
broadcast
();
final
_errorController
=
StreamController
<
String
>.
broadcast
();
Stream
<
LakiMqttConnectionState
>
get
stateStream
=>
_stateController
.
stream
;
Stream
<
LakiMqttMessage
>
get
messageStream
=>
_messageController
.
stream
;
Stream
<
String
>
get
errorStream
=>
_errorController
.
stream
;
bool
get
isConnected
=>
_client
?.
connectionStatus
?.
state
==
MqttConnectionState
.
connected
;
Future
<
void
>
connect
()
async
{
if
(!
_config
.
isConfigured
)
{
throw
StateError
(
'MQTT 连接配置未填写,请先配置 host 和 clientId'
);
}
if
(
isConnected
)
{
return
;
}
_emitState
(
LakiMqttConnectionState
.
connecting
);
final
client
=
MqttServerClient
(
_config
.
host
,
_config
.
clientId
);
client
.
port
=
_config
.
port
;
client
.
secure
=
_config
.
useSsl
;
client
.
useWebSocket
=
_config
.
useWebSocket
;
client
.
keepAlivePeriod
=
_config
.
keepAliveSeconds
;
client
.
connectTimeoutPeriod
=
_config
.
connectTimeoutMs
;
client
.
onConnected
=
()
=>
_emitState
(
LakiMqttConnectionState
.
connected
);
client
.
onDisconnected
=
()
=>
_emitState
(
LakiMqttConnectionState
.
disconnected
);
client
.
onSubscribed
=
(
topic
)
=>
_emitError
(
'MQTT 已订阅:
$topic
'
);
client
.
onSubscribeFail
=
(
topic
)
=>
_emitError
(
'MQTT 订阅失败:
$topic
'
);
client
.
pongCallback
=
()
{};
final
webSocketProtocols
=
_config
.
webSocketProtocols
;
if
(
webSocketProtocols
!=
null
)
{
client
.
websocketProtocols
=
webSocketProtocols
;
}
client
.
logging
(
on:
_config
.
logging
);
client
.
setProtocolV311
();
client
.
connectionMessage
=
MqttConnectMessage
()
.
withClientIdentifier
(
_config
.
clientId
)
.
startClean
()
.
withWillQos
(
MqttQos
.
atLeastOnce
);
try
{
await
client
.
connect
(
_config
.
username
,
_config
.
password
);
}
on
NoConnectionException
catch
(
error
)
{
client
.
disconnect
();
_emitState
(
LakiMqttConnectionState
.
failed
);
_emitError
(
'MQTT 连接失败:
$error
'
);
rethrow
;
}
on
SocketException
catch
(
error
)
{
client
.
disconnect
();
_emitState
(
LakiMqttConnectionState
.
failed
);
_emitError
(
'MQTT Socket 连接失败:
$error
'
);
rethrow
;
}
if
(
client
.
connectionStatus
?.
state
!=
MqttConnectionState
.
connected
)
{
final
status
=
client
.
connectionStatus
;
client
.
disconnect
();
_emitState
(
LakiMqttConnectionState
.
failed
);
throw
StateError
(
'MQTT 连接未成功:
$status
'
);
}
_client
=
client
;
_listenUpdates
(
client
);
_emitState
(
LakiMqttConnectionState
.
connected
);
}
void
subscribe
(
String
topic
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
})
{
_requireConnected
().
subscribe
(
topic
,
qos
);
}
void
unsubscribe
(
String
topic
)
{
_requireConnected
().
unsubscribe
(
topic
);
}
void
subscribeIcuCommands
(
String
deviceSn
)
{
subscribe
(
LakiMqttTopics
.
icuTo
(
deviceSn
));
}
void
subscribePetBind
(
String
deviceSn
)
{
subscribe
(
LakiMqttTopics
.
icuPetBind
(
deviceSn
));
}
int
publishJson
(
String
topic
,
Map
<
String
,
dynamic
>
payload
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
bool
retain
=
false
,
})
{
return
publishString
(
topic
,
jsonEncode
(
payload
),
qos:
qos
,
retain:
retain
,
);
}
int
publishString
(
String
topic
,
String
payload
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
bool
retain
=
false
,
})
{
final
builder
=
MqttClientPayloadBuilder
()..
addString
(
payload
);
return
_requireConnected
().
publishMessage
(
topic
,
qos
,
builder
.
payload
!,
retain:
retain
,
);
}
int
publishMonitorReport
(
McuReport
report
,
{
DateTime
?
date
,
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
})
{
if
(
report
.
sn
.
isEmpty
)
{
throw
ArgumentError
.
value
(
report
.
sn
,
'report.sn'
,
'设备 SN 不能为空'
);
}
return
publishJson
(
LakiMqttTopics
.
icuUp
(
report
.
sn
),
report
.
toMonitorPayload
(
date:
date
),
qos:
qos
,
);
}
int
publishCommandResponse
({
required
String
deviceSn
,
required
String
eventId
,
required
String
refEventId
,
required
int
status
,
required
String
message
,
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
})
{
return
publishJson
(
LakiMqttTopics
.
icuUp
(
deviceSn
),
{
'eventId'
:
eventId
,
'refEventId'
:
refEventId
,
'deviceType'
:
'ICU'
,
'timestamp'
:
DateTime
.
now
().
millisecondsSinceEpoch
,
'type'
:
'cmdResponse'
,
'data'
:
{
'status'
:
status
,
'msg'
:
message
,
},
},
qos:
qos
,
);
}
Future
<
void
>
disconnect
()
async
{
await
_updatesSub
?.
cancel
();
_updatesSub
=
null
;
_client
?.
disconnect
();
_client
=
null
;
_emitState
(
LakiMqttConnectionState
.
disconnected
);
}
Future
<
void
>
dispose
()
async
{
await
disconnect
();
await
_stateController
.
close
();
await
_messageController
.
close
();
await
_errorController
.
close
();
}
void
_listenUpdates
(
MqttServerClient
client
)
{
_updatesSub
?.
cancel
();
_updatesSub
=
client
.
updates
?.
listen
((
messages
)
{
for
(
final
item
in
messages
)
{
final
payload
=
item
.
payload
;
if
(
payload
is
!
MqttPublishMessage
)
{
continue
;
}
final
payloadText
=
MqttPublishPayload
.
bytesToStringAsString
(
payload
.
payload
.
message
,
);
_messageController
.
add
(
LakiMqttMessage
(
topic:
item
.
topic
,
payload:
payloadText
,
json:
_tryDecodeJson
(
payloadText
),
),
);
}
});
}
MqttServerClient
_requireConnected
()
{
final
client
=
_client
;
if
(
client
==
null
||
!
isConnected
)
{
throw
StateError
(
'MQTT 未连接'
);
}
return
client
;
}
void
_emitState
(
LakiMqttConnectionState
state
)
{
if
(!
_stateController
.
isClosed
)
{
_stateController
.
add
(
state
);
}
}
void
_emitError
(
String
message
)
{
if
(!
_errorController
.
isClosed
)
{
_errorController
.
add
(
message
);
}
}
Map
<
String
,
dynamic
>?
_tryDecodeJson
(
String
payload
)
{
try
{
final
decoded
=
jsonDecode
(
payload
);
return
decoded
is
Map
<
String
,
dynamic
>
?
decoded
:
null
;
}
catch
(
_
)
{
return
null
;
}
}
}
lib/utils/storage/storage_service.dart
View file @
531a4d4e
...
...
@@ -9,7 +9,11 @@ class StorageService {
static
const
String
_keyRememberSmsCode
=
'remember_sms_code'
;
static
const
String
_keyRememberEnabled
=
'remember_enabled'
;
static
const
String
_keyUserInfo
=
'user_info'
;
static
const
String
_keyUserMac
=
'user_mac'
;
static
const
String
_keyClientId
=
'client_id'
;
static
const
String
_keyBoundBluetoothDeviceId
=
'bound_bluetooth_device_id'
;
static
const
String
_keyBoundBluetoothDeviceName
=
'bound_bluetooth_device_name'
;
final
FlutterSecureStorage
_storage
=
const
FlutterSecureStorage
(
aOptions:
AndroidOptions
(
...
...
@@ -76,6 +80,7 @@ class StorageService {
Future
<
UserModel
?>
getUserInfo
()
async
{
final
value
=
await
_storage
.
read
(
key:
_keyUserInfo
);
UserModel
.
MAC
=
await
getUserMac
();
if
(
value
==
null
||
value
.
isEmpty
)
return
null
;
final
parts
=
value
.
split
(
'|'
);
if
(
parts
.
length
<
9
)
return
null
;
...
...
@@ -96,6 +101,22 @@ class StorageService {
await
_storage
.
delete
(
key:
_keyUserInfo
);
}
Future
<
void
>
saveUserMac
(
String
mac
)
async
{
UserModel
.
MAC
=
mac
;
await
_storage
.
write
(
key:
_keyUserMac
,
value:
mac
);
}
Future
<
String
?>
getUserMac
()
async
{
final
mac
=
await
_storage
.
read
(
key:
_keyUserMac
);
UserModel
.
MAC
=
mac
;
return
mac
;
}
Future
<
void
>
deleteUserMac
()
async
{
UserModel
.
MAC
=
null
;
await
_storage
.
delete
(
key:
_keyUserMac
);
}
// ==================== ClientId 相关 ====================
Future
<
void
>
saveClientId
(
String
clientId
)
async
{
...
...
@@ -110,6 +131,30 @@ class StorageService {
await
_storage
.
delete
(
key:
_keyClientId
);
}
// ==================== 蓝牙绑定相关 ====================
Future
<
void
>
saveBoundBluetoothDevice
({
required
String
deviceId
,
required
String
deviceName
,
})
async
{
await
_storage
.
write
(
key:
_keyBoundBluetoothDeviceId
,
value:
deviceId
);
await
_storage
.
write
(
key:
_keyBoundBluetoothDeviceName
,
value:
deviceName
);
}
Future
<
Map
<
String
,
String
?>>
getBoundBluetoothDevice
()
async
{
final
deviceId
=
await
_storage
.
read
(
key:
_keyBoundBluetoothDeviceId
);
final
deviceName
=
await
_storage
.
read
(
key:
_keyBoundBluetoothDeviceName
);
return
{
'deviceId'
:
deviceId
,
'deviceName'
:
deviceName
,
};
}
Future
<
void
>
deleteBoundBluetoothDevice
()
async
{
await
_storage
.
delete
(
key:
_keyBoundBluetoothDeviceId
);
await
_storage
.
delete
(
key:
_keyBoundBluetoothDeviceName
);
}
// ==================== 记住密码相关 ====================
Future
<
void
>
saveRememberCredentials
({
...
...
lib/views/home/index/cubit/home_index_cubit.dart
View file @
531a4d4e
...
...
@@ -6,21 +6,25 @@ 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'
;
/// 设备凭据 —— Mock 阶段使用占位值,后续接口接入后替换
/// TODO: 从舱详情接口获取真实的 cameraSn 和 wifiPwd
const
_mockDeviceNo
=
'MOCK_DEVICE_SN'
;
const
_mockDevicePassword
=
'MOCK_WIFI_PWD'
;
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,
/// 防止旧连接的异步结果污染当前模式的状态。
...
...
@@ -30,9 +34,14 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
:
_monitoringService
=
MonitoringService
(),
_webrtcService
=
WebrtcService
(),
_p2pVideoService
=
P2pVideoService
(),
_storageService
=
StorageService
(),
_bluetoothManager
=
BleBluetoothManager
(),
_mcuReportDecoder
=
McuReportFrameDecoder
(),
super
(
const
HomeIndexState
())
{
_listenWebrtcState
();
_listenP2pState
();
_listenBluetoothState
();
_loadBoundBluetoothDevice
();
loadData
();
}
...
...
@@ -54,6 +63,64 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
});
}
/// 监听蓝牙扫描、连接和数据状态
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
{
...
...
@@ -203,6 +270,81 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
}
}
// ==================== 蓝牙扫描/绑定 ====================
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
;
...
...
@@ -213,8 +355,12 @@ class HomeIndexCubit extends Cubit<HomeIndexState> {
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
()
);
}
}
lib/views/home/index/cubit/home_index_state.dart
View file @
531a4d4e
...
...
@@ -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
HomeIndexStatus
{
initial
,
...
...
@@ -31,6 +32,36 @@ class HomeIndexState 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
HomeIndexState
({
this
.
status
=
HomeIndexStatus
.
initial
,
this
.
isLoading
=
false
,
...
...
@@ -43,6 +74,16 @@ class HomeIndexState extends Equatable {
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
({
...
...
@@ -57,6 +98,18 @@ class HomeIndexState 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
HomeIndexState
(
status:
status
??
this
.
status
,
...
...
@@ -66,11 +119,28 @@ class HomeIndexState 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 HomeIndexState extends Equatable {
videoStreamMode
,
isSwitchingMode
,
isP2pConnected
,
isBluetoothScanning
,
isBluetoothBinding
,
bindingBluetoothDeviceId
,
bluetoothDevices
,
boundBluetoothDeviceId
,
boundBluetoothDeviceName
,
bluetoothConnectionStatus
,
bluetoothMessage
,
latestBluetoothRawHex
,
latestMcuReport
,
];
}
lib/views/home/index/home_index_view.dart
View file @
531a4d4e
...
...
@@ -8,6 +8,7 @@ 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'
;
...
...
@@ -49,7 +50,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
body:
SafeArea
(
child:
Column
(
children:
[
_buildAppBar
(),
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
buildWhen:
(
previous
,
current
)
=>
previous
.
boundBluetoothDeviceId
!=
current
.
boundBluetoothDeviceId
,
builder:
(
context
,
state
)
=>
_buildAppBar
(
state
),
),
Expanded
(
child:
BlocBuilder
<
HomeIndexCubit
,
HomeIndexState
>(
builder:
(
context
,
state
)
{
...
...
@@ -92,7 +98,9 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
}
Widget
_buildAppBar
()
{
Widget
_buildAppBar
(
HomeIndexState
state
)
{
final
isBluetoothBound
=
state
.
boundBluetoothDeviceId
?.
isNotEmpty
==
true
;
return
Container
(
padding:
EdgeInsets
.
symmetric
(
horizontal:
24
.
w
,
vertical:
12
.
h
),
decoration:
const
BoxDecoration
(
...
...
@@ -106,7 +114,8 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
const
CircleAvatar
(
backgroundColor:
Colors
.
white
,
radius:
24
,
child:
Icon
(
Icons
.
local_hospital
,
color:
Color
(
0xFF1E10B6
),
size:
28
),
child:
Icon
(
Icons
.
local_hospital
,
color:
Color
(
0xFF1E10B6
),
size:
28
),
),
SizedBox
(
width:
16
.
w
),
Text
(
...
...
@@ -118,7 +127,12 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
),
),
const
Spacer
(),
_buildAppBarIcon
(
Icons
.
bluetooth
,
'TSAAS'
),
_buildAppBarIcon
(
Icons
.
bluetooth
,
'TSAAS'
,
isActive:
isBluetoothBound
,
onTap:
_showBluetoothDialog
,
),
SizedBox
(
width:
16
.
w
),
_buildAppBarIcon
(
Icons
.
description
,
null
),
SizedBox
(
width:
16
.
w
),
...
...
@@ -128,28 +142,53 @@ class _HomeIndexContentState extends State<HomeIndexContent> {
);
}
Widget
_buildAppBarIcon
(
IconData
icon
,
String
?
label
)
{
return
Container
(
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
(
border:
Border
.
all
(
color:
Colors
.
white24
,
width:
1
),
color:
backgroundColor
,
border:
Border
.
all
(
color:
borderColor
,
width:
1
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
),
child:
Row
(
children:
[
Icon
(
icon
,
color:
Colors
.
white
,
size:
20
.
w
),
Icon
(
icon
,
color:
foregroundColor
,
size:
20
.
w
),
if
(
label
!=
null
)
...[
SizedBox
(
width:
6
.
w
),
Text
(
label
,
style:
TextStyle
(
fontSize:
18
.
sp
,
color:
Colors
.
white
,
color:
foregroundColor
,
fontWeight:
isActive
?
FontWeight
.
w700
:
FontWeight
.
w400
,
),
),
],
],
),
),
);
}
void
_showBluetoothDialog
()
{
showDialog
<
void
>(
context:
context
,
builder:
(
_
)
=>
BlocProvider
.
value
(
value:
_homeIndexCubit
,
child:
const
BluetoothBindDialog
(),
),
);
}
...
...
lib/views/home/index/widgets/bluetooth_bind_dialog.dart
0 → 100644
View file @
531a4d4e
This diff is collapsed.
Click to expand it.
macos/Flutter/GeneratedPluginRegistrant.swift
View file @
531a4d4e
...
...
@@ -9,6 +9,7 @@ import device_info_plus
import
flutter_image_compress_macos
import
flutter_secure_storage_macos
import
flutter_webrtc
import
reactive_ble_mobile
import
share_plus
import
shared_preferences_foundation
import
url_launcher_macos
...
...
@@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterImageCompressMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterImageCompressMacosPlugin"
))
FlutterSecureStoragePlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterSecureStoragePlugin"
))
FlutterWebRTCPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"FlutterWebRTCPlugin"
))
ReactiveBlePlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"ReactiveBlePlugin"
))
SharePlusMacosPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharePlusMacosPlugin"
))
SharedPreferencesPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"SharedPreferencesPlugin"
))
UrlLauncherPlugin
.
register
(
with
:
registry
.
registrar
(
forPlugin
:
"UrlLauncherPlugin"
))
...
...
pubspec.yaml
View file @
531a4d4e
...
...
@@ -50,16 +50,15 @@ dependencies:
logger
:
^2.7.0
fluttertoast
:
^9.0.0
permission_handler
:
^11.3.1
flutter_usbcamera
:
^0.0.1
flutter_usbcamera
:
path
:
./third_party/flutter_usbcamera
path_provider
:
^2.1.2
flutter_webrtc
:
path
:
./plugins/flutter_webrtc
vsdk
:
path
:
./plugins/vsdk
# flutter_usbcamera: ^0.0.1
# fl_chart: ^0.71.0
# fl_chart: ^1.1.0
flutter_reactive_ble
:
^5.5.0
mqtt_client
:
^10.5.1
dev_dependencies
:
flutter_test
:
sdk
:
flutter
...
...
@@ -84,6 +83,7 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter
:
default-flavor
:
dev
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
...
...
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