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
3e1af08e
Commit
3e1af08e
authored
Jun 25, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
对接完成 B端绑定宠物,mqtt下发到设备
parent
fbfad9ea
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
187 additions
and
72 deletions
+187
-72
cabin_repository.dart
lib/repositories/cabin_repository.dart
+5
-4
laki_mqtt_client.dart
lib/utils/mqtt/laki_mqtt_client.dart
+44
-2
monitoring_index_cubit.dart
lib/views/monitoring/index/cubit/monitoring_index_cubit.dart
+101
-34
monitoring_pet_info_card.dart
...ws/monitoring/index/widgets/monitoring_pet_info_card.dart
+37
-32
No files found.
lib/repositories/cabin_repository.dart
View file @
3e1af08e
...
@@ -15,12 +15,13 @@ class CabinRepository {
...
@@ -15,12 +15,13 @@ class CabinRepository {
Future
<
ResponseModel
<
CabinDetailResponse
>>
getCabinDetail
(
Future
<
ResponseModel
<
CabinDetailResponse
>>
getCabinDetail
(
String
carbinSn
,
String
carbinSn
,
)
{
)
{
if
(
Constants
.
useMockData
)
{
//
if (Constants.useMockData) {
return
_mockCabinDetail
();
//
return _mockCabinDetail();
}
//
}
return
DioRequest
.
instance
.
get
<
CabinDetailResponse
>(
return
DioRequest
.
instance
.
get
<
CabinDetailResponse
>(
'/icu/detail'
,
'/icu/detail'
,
queryParameters:
{
'carbinSn'
:
carbinSn
},
// queryParameters: {'carbinSn': carbinSn},
queryParameters:
{
'carbinSn'
:
'CNA07212L'
},
fromJsonT:
(
data
)
=>
fromJsonT:
(
data
)
=>
CabinDetailResponse
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
CabinDetailResponse
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
);
...
...
lib/utils/mqtt/laki_mqtt_client.dart
View file @
3e1af08e
import
'dart:async'
;
import
'dart:async'
;
import
'dart:convert'
;
import
'dart:convert'
;
import
'dart:io'
;
import
'dart:io'
;
import
'dart:math'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
import
'package:mqtt_client/mqtt_client.dart'
;
import
'package:mqtt_client/mqtt_client.dart'
;
...
@@ -69,11 +70,16 @@ class LakiMqttTopics {
...
@@ -69,11 +70,16 @@ class LakiMqttTopics {
static
String
icuUp
(
String
deviceSn
)
=>
'icu/
$deviceSn
/up'
;
static
String
icuUp
(
String
deviceSn
)
=>
'icu/
$deviceSn
/up'
;
static
String
cameraUp
(
String
deviceSn
)
=>
'camera/
$deviceSn
/up'
;
// stephen 上行 宠物异常行为处理结果 camera/{SN}/up → CameraResultHandler
static
String
icuAlarm
(
String
deviceSn
)
=>
'icu/
$deviceSn
/alarm'
;
// stephen 上行 传感器(设备)异常告警 icu/{SN}/alarm → DeviceAlarmHandler
static
String
icuTo
(
String
deviceSn
)
=>
'icu/
$deviceSn
/to'
;
static
String
icuTo
(
String
deviceSn
)
=>
'icu/
$deviceSn
/to'
;
static
String
icuPetBind
(
String
deviceSn
)
=>
'icu/
$deviceSn
/pet/bind'
;
static
String
icuPetBind
(
String
deviceSn
)
=>
'icu/
$deviceSn
/pet/bind'
;
// stephen 下行 宠物 绑定、出舱
static
String
cameraTo
(
String
cabinSn
)
=>
'camera/
$cabinSn
/to'
;
// 获取摄像头通道的告警信息
static
String
cameraTo
(
String
cabinSn
)
=>
'camera/
$cabinSn
/to'
;
//
stephen 下行
获取摄像头通道的告警信息
static
const
String
clientConnected
=
r'$events/client_connected'
;
static
const
String
clientConnected
=
r'$events/client_connected'
;
...
@@ -175,6 +181,11 @@ class LakiMqttClient {
...
@@ -175,6 +181,11 @@ class LakiMqttClient {
_client
=
client
;
_client
=
client
;
_listenUpdates
(
client
);
_listenUpdates
(
client
);
_emitState
(
LakiMqttConnectionState
.
connected
);
_emitState
(
LakiMqttConnectionState
.
connected
);
// ignore: avoid_print
print
(
'[LakiMqttClient] ✅ MQTT 已连接成功, clientId=
${_config.clientId}
'
'host=
${_config.host}
'
,
);
}
}
void
subscribe
(
String
topic
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
})
{
void
subscribe
(
String
topic
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
})
{
...
@@ -263,6 +274,37 @@ class LakiMqttClient {
...
@@ -263,6 +274,37 @@ class LakiMqttClient {
);
);
}
}
/// 上行 出舱数据
/// create by stephen
/// [deviceSn] 设备 SN,[type] 操作类型(unbindPet),
/// [petData] 宠物信息,为 null 时发送空对象。
// TODO: 服务端尚未确定具体数据结构,等确定后再调整
int
publishPetUnBind
({
required
String
deviceSn
,
required
String
type
,
required
Map
<
String
,
dynamic
>?
petData
,
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
})
{
return
publishJson
(
LakiMqttTopics
.
icuPetBind
(
deviceSn
),
{
'eventId'
:
'
${DateTime.now().millisecondsSinceEpoch}
_
${_randomHex(4)}
'
,
'deviceId'
:
deviceSn
,
'deviceType'
:
'ICU'
,
'timestamp'
:
DateTime
.
now
().
millisecondsSinceEpoch
,
'type'
:
type
,
'data'
:
petData
??
{},
},
qos:
qos
,
);
}
/// 生成指定长度的随机 hex 字符串
String
_randomHex
(
int
length
)
{
final
random
=
Random
();
return
List
.
generate
(
length
,
(
_
)
=>
random
.
nextInt
(
16
).
toRadixString
(
16
)).
join
();
}
Future
<
void
>
disconnect
()
async
{
Future
<
void
>
disconnect
()
async
{
await
_updatesSub
?.
cancel
();
await
_updatesSub
?.
cancel
();
_updatesSub
=
null
;
_updatesSub
=
null
;
...
...
lib/views/monitoring/index/cubit/monitoring_index_cubit.dart
View file @
3e1af08e
...
@@ -3,7 +3,7 @@ import 'dart:async';
...
@@ -3,7 +3,7 @@ import 'dart:async';
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:laki_icu_app/enums/video_stream_mode_enum.dart'
;
import
'package:laki_icu_app/enums/video_stream_mode_enum.dart'
;
import
'package:laki_icu_app/models/bo/cabin_detail_response.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.dart'
;
import
'package:laki_icu_app/models/bo/camera_alarm_bo.dart'
;
import
'package:laki_icu_app/models/bo/camera_alarm_bo.dart'
;
import
'package:laki_icu_app/services/cabin_service.dart'
;
import
'package:laki_icu_app/services/cabin_service.dart'
;
...
@@ -75,6 +75,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -75,6 +75,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_listenBluetoothReadInfo
();
_listenBluetoothReadInfo
();
_listenMcuReportChanged
();
_listenMcuReportChanged
();
_loadBoundBluetoothDevice
();
_loadBoundBluetoothDevice
();
// todo 测试使用 待删除
_fetchCabinDetailAndConnectVideo
(
"CNA07212L"
);
_connectMqttForDeviceSn
(
"CNA07212L"
);
}
}
/// 监听 WebRTC 连接状态变化并同步到 State
/// 监听 WebRTC 连接状态变化并同步到 State
...
@@ -150,7 +154,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -150,7 +154,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
).
copyWith
(
sn:
reportSn
.
isEmpty
?
null
:
reportSn
);
).
copyWith
(
sn:
reportSn
.
isEmpty
?
null
:
reportSn
);
if
(
bluetoothReadInfo
.
hasSn
)
{
if
(
bluetoothReadInfo
.
hasSn
)
{
await
_storageService
.
saveBluetoothReadSn
(
bluetoothReadInfo
.
sn
!);
await
_storageService
.
saveBluetoothReadSn
(
bluetoothReadInfo
.
sn
!);
await
_connectMqttForDeviceSn
(
bluetoothReadInfo
.
sn
!);
await
_connectMqttForDeviceSn
(
bluetoothReadInfo
.
sn
!);
// stephen 需要保留,选择蓝牙之后获取到sn就需要连接上mqtt
await
_fetchCabinDetailAndConnectVideo
(
bluetoothReadInfo
.
sn
!);
await
_fetchCabinDetailAndConnectVideo
(
bluetoothReadInfo
.
sn
!);
}
}
eventBus
.
emit
(
eventBus
.
emit
(
...
@@ -266,8 +270,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -266,8 +270,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
/// 页面初始化入口。
/// 页面初始化入口。
///
///
/// 数据不再通过 HTTP 获取:
/// 数据不再通过 HTTP 获取:
/// - 宠物信息由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 宠物信息由 MQTT icu/{SN}/pet/bind 推送获取
/// - 告警信息由 MQTT camera/{sn}/to 推送获取
/// - 摄像头凭证由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{SN}/to 推送获取
/// - 指标数据由蓝牙 MCU 上报实时填充
/// - 指标数据由蓝牙 MCU 上报实时填充
Future
<
void
>
loadData
()
async
{}
Future
<
void
>
loadData
()
async
{}
...
@@ -343,12 +348,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -343,12 +348,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final
detail
=
await
_cabinService
.
getCabinDetail
(
carbinSn
);
final
detail
=
await
_cabinService
.
getCabinDetail
(
carbinSn
);
if
(
isClosed
)
return
;
if
(
isClosed
)
return
;
// 仅从舱详情接口获取 cameraSn / wifiPwd,宠物信息改由 MQTT pet/bind 推送
emit
(
state
.
copyWith
(
emit
(
state
.
copyWith
(
cabinCameraSn:
detail
.
cameraSn
,
cabinCameraSn:
detail
.
cameraSn
,
cabinWifiPwd:
detail
.
wifiPwd
,
cabinWifiPwd:
detail
.
wifiPwd
,
patientInfo:
detail
.
petInfo
!=
null
?
_mapPetInfoToPatientInfo
(
detail
.
petInfo
!)
:
state
.
patientInfo
,
));
));
// 凭证就绪后发起视频连接
// 凭证就绪后发起视频连接
...
@@ -361,21 +364,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -361,21 +364,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
}
}
}
/// 将 [PetInfoResponse] 映射为 [PatientInfoBO]
PatientInfoBO
_mapPetInfoToPatientInfo
(
PetInfoResponse
pet
)
{
return
PatientInfoBO
(
name:
(
pet
.
petName
??
pet
.
name
)
??
''
,
type:
(
pet
.
petType
??
pet
.
type
)
??
''
,
phone:
pet
.
ownerPhone
??
''
,
breed:
pet
.
breedTag
??
''
,
doctor:
'缺失字段'
,
avatarUrl:
pet
.
avatar
??
''
,
disease:
pet
.
disease
??
''
,
checkInDate:
'缺失字段'
,
careLevel:
'缺失字段'
,
);
}
// ==================== 模式切换 ====================
// ==================== 模式切换 ====================
/// 切换视频传输模式
/// 切换视频传输模式
...
@@ -691,6 +679,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -691,6 +679,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_handleCameraAlarmMessage
(
json
);
_handleCameraAlarmMessage
(
json
);
return
;
return
;
}
}
if
(
json
!=
null
&&
message
.
topic
==
LakiMqttTopics
.
icuPetBind
(
_mqttDeviceSn
??
''
))
{
_handlePetBindMessage
(
json
);
return
;
}
emit
(
state
.
copyWith
(
emit
(
state
.
copyWith
(
mqttMessage:
'MQTT 收到
${message.topic}
:
${message.payload}
'
,
mqttMessage:
'MQTT 收到
${message.topic}
:
${message.payload}
'
,
));
));
...
@@ -730,6 +723,46 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -730,6 +723,46 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
));
));
}
}
/// 处理 MQTT pet/bind 宠物绑定消息
///
/// 服务端推送的宠物绑定数据格式:
/// ```json
/// {
/// "type": "bindPet",
/// "data": { "name": "旺财", "type": "dog", "breedTag": "金毛", ... }
/// }
/// ```
/// [type] 为 `bindPet` 时设置 petInfo,为 `unbindPet` 时清空 petInfo
void
_handlePetBindMessage
(
Map
<
String
,
dynamic
>
json
)
{
final
type
=
json
[
'type'
]
as
String
?;
if
(
type
==
null
)
return
;
if
(
type
==
'unbindPet'
)
{
// 出舱:清空宠物信息
emit
(
state
.
copyWith
(
clearPatientInfo:
true
));
return
;
}
if
(
type
!=
'bindPet'
)
return
;
final
data
=
json
[
'data'
];
if
(
data
is
!
Map
<
String
,
dynamic
>)
return
;
final
patientInfo
=
PatientInfoBO
(
name:
(
data
[
'name'
]
as
String
?)
??
''
,
type:
(
data
[
'type'
]
as
String
?)
??
''
,
phone:
(
data
[
'ownerPhone'
]
as
String
?)
??
''
,
breed:
(
data
[
'breedTag'
]
as
String
?)
??
''
,
doctor:
'缺失字段'
,
avatarUrl:
(
data
[
'avatar'
]
as
String
?)
??
''
,
disease:
(
data
[
'disease'
]
as
String
?)
??
''
,
checkInDate:
(
data
[
'createTime'
]
as
String
?)
??
'缺失字段'
,
careLevel:
'缺失字段'
,
);
emit
(
state
.
copyWith
(
patientInfo:
patientInfo
));
}
/// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致)
/// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致)
String
_formatTimestamp
(
int
milliseconds
)
{
String
_formatTimestamp
(
int
milliseconds
)
{
final
dateTime
=
final
dateTime
=
...
@@ -952,26 +985,60 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
...
@@ -952,26 +985,60 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
}
/// 清空宠物信息(出舱)
/// 清空宠物信息(出舱)
///
/// 通过 MQTT 上行出舱数据后清空 state 中的宠物信息。
void
clearPatientInfo
()
{
void
clearPatientInfo
()
{
if
(
isClosed
)
return
;
if
(
isClosed
)
return
;
final
deviceSn
=
state
.
mqttDeviceSn
;
final
client
=
_mqttClient
;
debugPrint
(
'[出舱] deviceSn=
$deviceSn
mqttClient=
${client != null}
isConnected=
${client?.isConnected ?? false}
patientInfo=
${state.patientInfo?.name}
'
);
if
(
deviceSn
!=
null
&&
deviceSn
.
isNotEmpty
&&
client
!=
null
&&
client
.
isConnected
)
{
try
{
final
msgId
=
client
.
publishPetUnBind
(
deviceSn:
deviceSn
,
type:
'unbindPet'
,
// TODO: 出舱 type 后续等服务端确定后再调整
petData:
_patientInfoToMap
(),
);
debugPrint
(
'[出舱] MQTT publishPetUnBind 成功 msgId=
$msgId
topic=icu/
$deviceSn
/pet/bind'
);
}
catch
(
e
)
{
debugPrint
(
'[出舱] MQTT publishPetUnBind 失败:
$e
'
);
if
(!
isClosed
)
{
emit
(
state
.
copyWith
(
mqttMessage:
'MQTT 出舱上报失败:
$e
'
));
}
}
}
else
{
debugPrint
(
'[出舱] 跳过 MQTT 上行: '
'deviceSn=
${deviceSn ?? "null"}
'
'hasClient=
${client != null}
'
'isConnected=
${client?.isConnected ?? false}
'
);
}
emit
(
state
.
copyWith
(
clearPatientInfo:
true
));
emit
(
state
.
copyWith
(
clearPatientInfo:
true
));
}
}
/// 将当前宠物信息转为 Map,供 MQTT 上行使用
Map
<
String
,
dynamic
>?
_patientInfoToMap
()
{
final
info
=
state
.
patientInfo
;
if
(
info
==
null
)
return
null
;
return
{
'name'
:
info
.
name
,
'petType'
:
info
.
type
,
'phone'
:
info
.
phone
,
'breed'
:
info
.
breed
,
'doctor'
:
info
.
doctor
,
'disease'
:
info
.
disease
,
'checkInDate'
:
info
.
checkInDate
,
'careLevel'
:
info
.
careLevel
,
};
}
/// 设置测试宠物信息(绑定宠物)
/// 设置测试宠物信息(绑定宠物)
void
setTestPatientInfo
()
{
void
setTestPatientInfo
()
{
if
(
isClosed
)
return
;
if
(
isClosed
)
return
;
const
testPatientInfo
=
PatientInfoBO
(
// todo 平板端暂时不做宠物绑定 stephen
name:
'妮蔻'
,
type:
'猫'
,
phone:
'130-1111-3333'
,
breed:
'长毛三花'
,
doctor:
'张医生'
,
disease:
'胃炎'
,
checkInDate:
'2025-6-15'
,
careLevel:
'特级'
,
avatarUrl:
''
,
);
emit
(
state
.
copyWith
(
patientInfo:
testPatientInfo
));
}
}
/// 获取 WebRTC 渲染器供 UI 层使用
/// 获取 WebRTC 渲染器供 UI 层使用
...
...
lib/views/monitoring/index/widgets/monitoring_pet_info_card.dart
View file @
3e1af08e
...
@@ -181,39 +181,39 @@ class MonitoringPetInfoCard extends StatelessWidget {
...
@@ -181,39 +181,39 @@ class MonitoringPetInfoCard extends StatelessWidget {
),
),
SizedBox
(
height:
26
.
h
),
SizedBox
(
height:
26
.
h
),
// 绑定宠物按钮
// 绑定宠物按钮
GestureDetector
(
//
GestureDetector(
onTap:
onBindPet
,
//
onTap: onBindPet,
child:
Container
(
//
child: Container(
width:
double
.
infinity
,
//
width: double.infinity,
height:
140
.
h
,
//
height: 140.h,
decoration:
const
BoxDecoration
(
//
decoration: const BoxDecoration(
image:
DecorationImage
(
//
image: DecorationImage(
image:
AssetImage
(
'lib/assets/monitoring/title_bg_spacewalk.png'
),
//
image: AssetImage('lib/assets/monitoring/title_bg_spacewalk.png'),
fit:
BoxFit
.
fill
,
//
fit: BoxFit.fill,
),
//
),
),
//
),
child:
Center
(
//
child: Center(
child:
Row
(
//
child: Row(
mainAxisAlignment:
MainAxisAlignment
.
center
,
//
mainAxisAlignment: MainAxisAlignment.center,
children:
[
//
children: [
SizedBox
(
width:
24
.
w
),
//
SizedBox(width: 24.w),
Text
(
//
Text(
'绑定宠物'
,
//
'绑定宠物',
style:
TextStyle
(
//
style: TextStyle(
color:
Colors
.
white
,
//
color: Colors.white,
fontSize:
32
.
sp
,
//
fontSize: 32.sp,
fontWeight:
FontWeight
.
w700
,
//
fontWeight: FontWeight.w700,
letterSpacing:
4
.
w
,
//
letterSpacing: 4.w,
),
//
),
),
//
),
SizedBox
(
width:
24
.
w
),
//
SizedBox(width: 24.w),
],
//
],
),
//
),
),
//
),
),
//
),
),
//
),
],
],
);
);
}
}
...
@@ -349,10 +349,15 @@ class MonitoringPetInfoCard extends StatelessWidget {
...
@@ -349,10 +349,15 @@ class MonitoringPetInfoCard extends StatelessWidget {
);
);
}
}
String
_truncate
(
String
text
,
{
int
maxChars
=
10
})
{
if
(
text
.
length
<=
maxChars
)
return
text
;
return
'
${text.substring(0, maxChars)}
...'
;
}
Widget
_buildInfoBlock
(
PatientInfoBO
info
)
{
Widget
_buildInfoBlock
(
PatientInfoBO
info
)
{
return
Column
(
return
Column
(
children:
[
children:
[
_buildInfoItem
(
Icons
.
description_outlined
,
'宠物病因'
,
info
.
disease
),
_buildInfoItem
(
Icons
.
description_outlined
,
'宠物病因'
,
_truncate
(
info
.
disease
)
),
SizedBox
(
height:
20
.
h
),
SizedBox
(
height:
20
.
h
),
_buildInfoItem
(
Icons
.
home_outlined
,
'入住时间'
,
info
.
checkInDate
),
_buildInfoItem
(
Icons
.
home_outlined
,
'入住时间'
,
info
.
checkInDate
),
SizedBox
(
height:
20
.
h
),
SizedBox
(
height:
20
.
h
),
...
...
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