Commit 83539172 authored by 张宏's avatar 张宏

22222222

parent d6fc2a2b
This diff is collapsed.
/// MQTT 摄像头告警消息数据模型
/// 对应 topic: camera/{cabinSn}/to 中 type="abnormal_behavior" 的消息
///
/// 与 Android 项目 qisuanfa_0621 MqttManager.processControlCommand() 保持一致:
/// - 只提取 behaviorCode 和 timestamp 两个字段用于标题映射
/// - 其余字段保留解析,方便后续扩展
class CameraAlarmBO {
/// 事件唯一标识(UUID 去横线),用于去重
final String eventId;
/// 仓 SN
final String deviceId;
/// 设备类型,固定 "camera"
final String deviceType;
/// 毫秒时间戳
final int timestamp;
/// 消息类型,如 "abnormal_behavior"
final String type;
/// 告警 ID
final int alarmId;
/// 告警类别
final String alarmType;
/// 行为编码
final int? behaviorCode;
/// 行为描述
final String? behaviorDesc;
/// 宠物名称
final String? petName;
const CameraAlarmBO({
required this.eventId,
required this.deviceId,
required this.deviceType,
required this.timestamp,
required this.type,
required this.alarmId,
required this.alarmType,
this.behaviorCode,
this.behaviorDesc,
this.petName,
});
factory CameraAlarmBO.fromJson(Map<String, dynamic> json) {
return CameraAlarmBO(
eventId: json['eventId'] as String? ?? '',
deviceId: json['deviceId'] as String? ?? '',
deviceType: json['deviceType'] as String? ?? '',
timestamp: json['timestamp'] as int? ?? 0,
type: json['type'] as String? ?? '',
alarmId: json['alarmId'] as int? ?? 0,
alarmType: json['alarmType'] as String? ?? '',
behaviorCode: json['behaviorCode'] as int?,
behaviorDesc: json['behaviorDesc'] as String?,
petName: json['petName'] as String?,
);
}
/// 与 Android 行为编码映射完全一致:
/// 2 → 异常液体
/// 3 → 检测到排泄物
/// 其他 → 未知异常
String get titleByBehaviorCode {
if (behaviorCode == 2) return '异常液体';
if (behaviorCode == 3) return '检测到排泄物';
return '未知异常';
}
}
......@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.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/camera_alarm_bo.dart';
import 'package:laki_icu_app/services/cabin_service.dart';
import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/p2p_video_service.dart';
......@@ -50,6 +51,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
bool _isMqttConnecting = false;
String? _mqttDeviceSn;
/// 已收到的摄像头告警 eventId 集合,用于去重
final Set<String> _seenAlarmEventIds = {};
/// 记录已请求过舱详情的 SN,避免每个 BLE 数据帧都重复请求接口
String? _lastFetchedCabinSn;
bool _isFetchingCabinDetail = false;
......@@ -563,7 +567,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
mqttMessage: 'MQTT 已断开',
bluetoothMessage: '蓝牙设备已解绑',
latestBluetoothRawHex: '',
cameraAlerts: const [],
));
_seenAlarmEventIds.clear();
_isManualUnbindingBluetooth = false;
}
......@@ -673,7 +679,8 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
await _mqttClient!.connect();
_mqttClient!
..subscribeIcuCommands(deviceSn)
..subscribePetBind(deviceSn);
..subscribePetBind(deviceSn)
..subscribe(LakiMqttTopics.cameraTo(deviceSn));
if (!isClosed) {
emit(state.copyWith(
......@@ -711,6 +718,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_mqttMessageSub = client.messageStream.listen((message) {
if (isClosed) return;
final json = message.json;
if (json != null &&
message.topic == LakiMqttTopics.cameraTo(_mqttDeviceSn ?? '')) {
_handleCameraAlarmMessage(json);
return;
}
emit(state.copyWith(
mqttMessage: 'MQTT 收到 ${message.topic}: ${message.payload}',
));
......@@ -722,6 +735,44 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
});
}
/// 处理摄像头告警消息(与 Android qisuanfa_0621 逻辑保持一致)
///
/// 仅处理 type == "abnormal_behavior" 的消息,
/// 按 behaviorCode 映射标题:2→异常液体、3→检测到排泄物、其他→未知异常
void _handleCameraAlarmMessage(Map<String, dynamic> json) {
if (json['type'] != 'abnormal_behavior') return;
final alarm = CameraAlarmBO.fromJson(json);
// 按 eventId 去重
if (_seenAlarmEventIds.contains(alarm.eventId)) return;
_seenAlarmEventIds.add(alarm.eventId);
final timeStr = _formatTimestamp(alarm.timestamp);
final title = alarm.titleByBehaviorCode;
final alert = AlertInfoBO(
title: title,
description: title,
time: timeStr,
status: '待处理',
);
emit(state.copyWith(
cameraAlerts: [alert, ...state.cameraAlerts],
));
}
/// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致)
String _formatTimestamp(int milliseconds) {
final dateTime =
DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: true);
final local = dateTime.toLocal();
return '${local.hour.toString().padLeft(2, '0')}:'
'${local.minute.toString().padLeft(2, '0')}:'
'${local.second.toString().padLeft(2, '0')}';
}
void _publishMqttMonitorReport(McuReport report) {
final client = _mqttClient;
if (client == null || !client.isConnected || report.sn.isEmpty) {
......
......@@ -77,6 +77,9 @@ class MonitoringIndexState extends Equatable {
/// 设备 WiFi 密码(从舱详情接口 /cabin/detail 获取)
final String? cabinWifiPwd;
/// MQTT 实时推送的摄像头告警列表(来自 camera/{sn}/to)
final List<AlertInfoBO> cameraAlerts;
/// 当前选中的告警(用于弹窗显示)
final AlertInfoBO? selectedAlert;
......@@ -87,6 +90,7 @@ class MonitoringIndexState extends Equatable {
this.metrics = const [],
this.patientInfo,
this.alerts = const [],
this.cameraAlerts = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected,
this.videoStreamMode = VideoStreamMode.p2p,
this.isSwitchingMode = false,
......@@ -116,6 +120,7 @@ class MonitoringIndexState extends Equatable {
List<MonitoringMetricBO>? metrics,
PatientInfoBO? patientInfo,
List<AlertInfoBO>? alerts,
List<AlertInfoBO>? cameraAlerts,
WebrtcConnectionState? videoConnectionState,
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
......@@ -148,6 +153,7 @@ class MonitoringIndexState extends Equatable {
metrics: metrics ?? this.metrics,
patientInfo: clearPatientInfo ? null : patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
cameraAlerts: cameraAlerts ?? this.cameraAlerts,
videoConnectionState: videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
......@@ -188,6 +194,7 @@ class MonitoringIndexState extends Equatable {
metrics,
patientInfo,
alerts,
cameraAlerts,
videoConnectionState,
videoStreamMode,
isSwitchingMode,
......
......@@ -129,7 +129,12 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
children: [
Expanded(
flex: 10,
child: MonitoringAlertCard(alerts: state.alerts),
child: MonitoringAlertCard(
alerts: [
...state.cameraAlerts,
...state.alerts,
],
),
),
SizedBox(width: 10.w),
Expanded(
......
......@@ -23,21 +23,15 @@ class MonitoringAlertDetailDialog extends StatelessWidget {
Widget _buildDialogContent(BuildContext context) {
return Container(
width: 1300.w,
height: 800.h,
margin: EdgeInsets.fromLTRB(100.w, 100.h, 100.w, 100.h),
decoration: BoxDecoration(
color: const Color(0xFF0A1929),
border: Border.all(
color: const Color(0xFF00D4FF),
width: 3.w,
image: const DecorationImage(
image: AssetImage('lib/assets/monitoring/dialog_alert_bg.png'),
fit: BoxFit.fill,
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: const Color(0xFF00D4FF).withValues(alpha: 77),
blurRadius: 20.r,
offset: const Offset(0, 0),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -227,7 +221,9 @@ class MonitoringAlertDetailDialog extends StatelessWidget {
VoidCallback onPressed, {
bool isActive = false,
}) {
return Container(
return GestureDetector(
onTap: onPressed,
child: Container(
width: 220.w,
height: 72.h,
decoration: BoxDecoration(
......@@ -247,8 +243,7 @@ class MonitoringAlertDetailDialog extends StatelessWidget {
]
: null,
),
child: TextButton(
onPressed: onPressed,
child: Center(
child: Text(
text,
style: TextStyle(
......@@ -258,6 +253,7 @@ class MonitoringAlertDetailDialog extends StatelessWidget {
),
),
),
),
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment