Commit 266ba06d authored by akari's avatar akari

feat: 控制温湿度

parent dc8a1afb
import 'dart:async';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'device_control_event.dart';
import 'device_control_state.dart';
import 'package:laki_icu_app/models/bo/device_control_bo.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:laki_icu_app/utils/event_bus.dart';
import 'package:laki_icu_app/utils/logger.dart';
import 'package:laki_icu_app/utils/storage/storage_service.dart';
/// 用途:设备控制Bloc
/// 涉及页面:设备控制页面
/// 说明:提供各个子模块的获取和设置方法,方便MCU单元调用
/// 使用 HydratedBloc 持久化用户设置,MCU获取的值不做持久化
class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlState> {
DeviceControlBloc() : super(const DeviceControlState()) {
class DeviceControlBloc
extends HydratedBloc<DeviceControlEvent, DeviceControlState> {
DeviceControlBloc({
BleBluetoothManager? bluetoothManager,
StorageService? storageService,
}) : _bluetoothManager = bluetoothManager ?? appBluetoothManager,
_storageService = storageService ?? StorageService(),
_mcuReportDecoder = McuReportFrameDecoder(),
super(const DeviceControlState()) {
// ==================== 基础事件 ====================
on<DeviceControlLoadRequested>(_onLoadRequested);
on<DeviceControlDataUpdated>(_onDataUpdated);
on<DeviceControlMcuReportFetched>(_onMcuReportFetched);
// ==================== 设备SN事件 ====================
on<DeviceControlSnFetched>(_onSnFetched);
on<DeviceControlSnChanged>(_onSnChanged);
// ==================== 舱内温度控制事件 ====================
on<DeviceControlCabinTempCurrentValueFetched>(_onCabinTempCurrentValueFetched);
on<DeviceControlCabinTempCurrentValueFetched>(
_onCabinTempCurrentValueFetched);
on<DeviceControlCabinTempSetValueChanged>(_onCabinTempSetValueChanged);
on<DeviceControlCabinTempSetValueAdjusted>(_onCabinTempSetValueAdjusted);
on<DeviceControlCabinTempSwitchChanged>(_onCabinTempSwitchChanged);
// ==================== 舱内湿度控制事件 ====================
on<DeviceControlCabinHumidityCurrentValueFetched>(_onCabinHumidityCurrentValueFetched);
on<DeviceControlCabinHumiditySetValueChanged>(_onCabinHumiditySetValueChanged);
on<DeviceControlCabinHumidityCurrentValueFetched>(
_onCabinHumidityCurrentValueFetched);
on<DeviceControlCabinHumiditySetValueChanged>(
_onCabinHumiditySetValueChanged);
on<DeviceControlCabinHumiditySetValueAdjusted>(
_onCabinHumiditySetValueAdjusted);
on<DeviceControlCabinHumiditySwitchChanged>(_onCabinHumiditySwitchChanged);
// ==================== 氧气浓度控制事件 ====================
on<DeviceControlOxygenConcentrationCurrentValueFetched>(_onOxygenConcentrationCurrentValueFetched);
on<DeviceControlOxygenConcentrationSetValueChanged>(_onOxygenConcentrationSetValueChanged);
on<DeviceControlOxygenConcentrationSwitchChanged>(_onOxygenConcentrationSwitchChanged);
on<DeviceControlOxygenConcentrationCurrentValueFetched>(
_onOxygenConcentrationCurrentValueFetched);
on<DeviceControlOxygenConcentrationSetValueChanged>(
_onOxygenConcentrationSetValueChanged);
on<DeviceControlOxygenConcentrationSetValueAdjusted>(
_onOxygenConcentrationSetValueAdjusted);
on<DeviceControlOxygenConcentrationSwitchChanged>(
_onOxygenConcentrationSwitchChanged);
// ==================== 二氧化碳浓度控制事件 ====================
on<DeviceControlCO2ConcentrationCurrentValueFetched>(_onCO2ConcentrationCurrentValueFetched);
on<DeviceControlCO2ConcentrationAlarmThresholdChanged>(_onCO2ConcentrationAlarmThresholdChanged);
on<DeviceControlCO2ConcentrationCurrentValueFetched>(
_onCO2ConcentrationCurrentValueFetched);
on<DeviceControlCO2ConcentrationAlarmThresholdChanged>(
_onCO2ConcentrationAlarmThresholdChanged);
on<DeviceControlCO2ConcentrationAlarmThresholdAdjusted>(
_onCO2ConcentrationAlarmThresholdAdjusted);
// ==================== 护理等级控制事件 ====================
on<DeviceControlCareLevelChanged>(_onCareLevelChanged);
......@@ -64,7 +93,8 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
// ==================== 灯光控制事件 ====================
on<DeviceControlCheckLightSwitchChanged>(_onCheckLightSwitchChanged);
on<DeviceControlIlluminationLightSwitchChanged>(_onIlluminationLightSwitchChanged);
on<DeviceControlIlluminationLightSwitchChanged>(
_onIlluminationLightSwitchChanged);
on<DeviceControlBlueLightSwitchChanged>(_onBlueLightSwitchChanged);
on<DeviceControlRedLightSwitchChanged>(_onRedLightSwitchChanged);
......@@ -75,9 +105,21 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
on<DeviceControlResetSettings>(_onResetSettings);
// 加载初始化数据
_mcuReportSub = eventBus.on<McuReportChangedEvent>().listen(
(event) => add(DeviceControlMcuReportFetched(event.report)),
);
_bluetoothDataSub = _bluetoothManager.dataStream.listen(
_onBluetoothDataPacket,
);
add(const DeviceControlLoadRequested());
}
final BleBluetoothManager _bluetoothManager;
final StorageService _storageService;
final McuReportFrameDecoder _mcuReportDecoder;
late final StreamSubscription<McuReportChangedEvent> _mcuReportSub;
late final StreamSubscription<BluetoothDataPacket> _bluetoothDataSub;
// ==================== HydratedBloc 持久化 ====================
@override
......@@ -98,9 +140,11 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
) async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
// 从存储中加载设备控制数据
// TODO: 实现从存储加载逻辑
emit(state.copyWith(isLoading: false, clearError: true));
emit(state.copyWith(
data: _clearMcuReadValues(state.data),
isLoading: false,
clearError: true,
));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
......@@ -113,6 +157,77 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
emit(state.copyWith(data: event.data, clearError: true));
}
DeviceControlBO _clearMcuReadValues(DeviceControlBO data) {
return data.copyWith(
cabinTemp: data.cabinTemp.copyWith(currentValue: '--'),
cabinHumidity: data.cabinHumidity.copyWith(currentValue: '--'),
oxygenConcentration:
data.oxygenConcentration.copyWith(currentValue: '--'),
co2Concentration: data.co2Concentration.copyWith(currentValue: '--'),
);
}
void _onBluetoothDataPacket(BluetoothDataPacket packet) {
try {
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG device_control_packet len=${packet.bytes.length} '
'hex=${packet.rawHex}',
);
final reports = _mcuReportDecoder.addBytes(packet.bytes);
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG device_control_parse reports=${reports.length} '
'buffered=${_mcuReportDecoder.bufferedBytes} '
'bufferHex=${_mcuReportDecoder.bufferedHexPreview}',
);
if (reports.isEmpty || isClosed) return;
final latestReport = reports.last;
eventBus.emit(McuReportChangedEvent(latestReport));
} catch (e) {
Log.warn('🔥🔥🔥 BLE_READ_DEBUG device_control_parse_error error=$e');
// 蓝牙原始数据可能分包或夹杂非MCU帧,这里只忽略控制页同步失败。
}
}
void _onMcuReportFetched(
DeviceControlMcuReportFetched event,
Emitter<DeviceControlState> emit,
) {
final report = event.report;
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG device_control_sync '
'frameLength=${report.length} '
'temp=${report.mainTemperature} '
'tempSet=${report.temperatureSetting} '
'humidity=${report.humidity} '
'humiditySet=${report.humiditySetting} '
'o2=${report.oxygenConcentration} '
'o2Set=${report.oxygenSetting} '
'co2=${report.co2Measurement} '
'co2Set=${report.co2Setting} '
'sn=${report.sn.isEmpty ? '<EMPTY>' : report.sn}',
);
final newData = state.data.copyWith(
sn: report.sn.isEmpty ? state.data.sn : report.sn,
cabinTemp: state.data.cabinTemp.copyWith(
currentValue: _formatTemperature(report.mainTemperature),
setValue: _formatTemperature(report.temperatureSetting),
),
cabinHumidity: state.data.cabinHumidity.copyWith(
currentValue: _formatInt(report.humidity),
setValue: _formatInt(report.humiditySetting),
),
oxygenConcentration: state.data.oxygenConcentration.copyWith(
currentValue: _formatInt(report.oxygenConcentration),
setValue: _formatInt(report.oxygenSetting),
),
co2Concentration: state.data.co2Concentration.copyWith(
currentValue: _formatInt(report.co2Measurement),
alarmThreshold: _formatInt(report.co2Setting),
),
);
emit(state.copyWith(data: newData, clearError: true));
}
// ==================== 设备SN事件处理 ====================
/// [MCU调用] 更新设备SN(从MCU获取的真实数据)
......@@ -158,21 +273,44 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
}
/// [用户操作] 设置舱内温度设定值(发送给MCU)—— 持久化
void _onCabinTempSetValueChanged(
Future<void> _onCabinTempSetValueChanged(
DeviceControlCabinTempSetValueChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
await _setCabinTempSetValue(event.setValue, emit);
}
/// [用户操作] 点击舱内温度设定值 -/+(发送给MCU)—— 持久化
Future<void> _onCabinTempSetValueAdjusted(
DeviceControlCabinTempSetValueAdjusted event,
Emitter<DeviceControlState> emit,
) async {
final currentValue = int.tryParse(state.data.cabinTemp.setValue) ?? 0;
final nextValue = (currentValue + event.step).clamp(0, 99);
await _setCabinTempSetValue('$nextValue', emit);
}
Future<void> _setCabinTempSetValue(
String setValue,
Emitter<DeviceControlState> emit,
) async {
final newData = state.data.copyWith(
cabinTemp: state.data.cabinTemp.copyWith(
setValue: event.setValue,
setValue: setValue,
),
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(cabinTempSetValue: event.setValue),
userSettings: state.userSettings.copyWith(cabinTempSetValue: setValue),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x02,
setValue: setValue,
errorPrefix: '舱内温度设定',
emit: emit,
scale: 10,
);
}
/// [用户操作] 设置舱内温度开关(发送给MCU)—— 持久化
......@@ -209,21 +347,44 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
}
/// [用户操作] 设置舱内湿度设定值(发送给MCU)—— 持久化
void _onCabinHumiditySetValueChanged(
Future<void> _onCabinHumiditySetValueChanged(
DeviceControlCabinHumiditySetValueChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
await _setCabinHumiditySetValue(event.setValue, emit);
}
/// [用户操作] 点击舱内湿度设定值 -/+(发送给MCU)—— 持久化
Future<void> _onCabinHumiditySetValueAdjusted(
DeviceControlCabinHumiditySetValueAdjusted event,
Emitter<DeviceControlState> emit,
) async {
final currentValue = int.tryParse(state.data.cabinHumidity.setValue) ?? 0;
final nextValue = (currentValue + event.step).clamp(0, 100);
await _setCabinHumiditySetValue('$nextValue', emit);
}
Future<void> _setCabinHumiditySetValue(
String setValue,
Emitter<DeviceControlState> emit,
) async {
final newData = state.data.copyWith(
cabinHumidity: state.data.cabinHumidity.copyWith(
setValue: event.setValue,
setValue: setValue,
),
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(cabinHumiditySetValue: event.setValue),
userSettings:
state.userSettings.copyWith(cabinHumiditySetValue: setValue),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x04,
setValue: setValue,
errorPrefix: '舱内湿度设定',
emit: emit,
);
}
/// [用户操作] 设置舱内湿度开关(发送给MCU)—— 持久化
......@@ -260,21 +421,45 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
}
/// [用户操作] 设置氧气浓度设定值(发送给MCU)—— 持久化
void _onOxygenConcentrationSetValueChanged(
Future<void> _onOxygenConcentrationSetValueChanged(
DeviceControlOxygenConcentrationSetValueChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
await _setOxygenConcentrationSetValue(event.setValue, emit);
}
/// [用户操作] 点击氧气浓度设定值 -/+(发送给MCU)—— 持久化
Future<void> _onOxygenConcentrationSetValueAdjusted(
DeviceControlOxygenConcentrationSetValueAdjusted event,
Emitter<DeviceControlState> emit,
) async {
final currentValue =
int.tryParse(state.data.oxygenConcentration.setValue) ?? 0;
final nextValue = (currentValue + event.step).clamp(0, 100);
await _setOxygenConcentrationSetValue('$nextValue', emit);
}
Future<void> _setOxygenConcentrationSetValue(
String setValue,
Emitter<DeviceControlState> emit,
) async {
final newData = state.data.copyWith(
oxygenConcentration: state.data.oxygenConcentration.copyWith(
setValue: event.setValue,
setValue: setValue,
),
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(oxygenConcentrationSetValue: event.setValue),
userSettings:
state.userSettings.copyWith(oxygenConcentrationSetValue: setValue),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x05,
setValue: setValue,
errorPrefix: '氧气浓度设定',
emit: emit,
);
}
/// [用户操作] 设置氧气浓度开关(发送给MCU)—— 持久化
......@@ -289,7 +474,8 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(oxygenConcentrationIsOn: event.isOn),
userSettings:
state.userSettings.copyWith(oxygenConcentrationIsOn: event.isOn),
clearError: true,
));
// TODO: 发送设置到MCU
......@@ -311,21 +497,45 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
}
/// [用户操作] 设置二氧化碳浓度告警阈值(发送给MCU)—— 持久化
void _onCO2ConcentrationAlarmThresholdChanged(
Future<void> _onCO2ConcentrationAlarmThresholdChanged(
DeviceControlCO2ConcentrationAlarmThresholdChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
await _setCO2ConcentrationAlarmThreshold(event.alarmThreshold, emit);
}
/// [用户操作] 点击二氧化碳浓度设定值 -/+(发送给MCU)—— 持久化
Future<void> _onCO2ConcentrationAlarmThresholdAdjusted(
DeviceControlCO2ConcentrationAlarmThresholdAdjusted event,
Emitter<DeviceControlState> emit,
) async {
final currentValue =
int.tryParse(state.data.co2Concentration.alarmThreshold) ?? 0;
final nextValue = (currentValue + event.step).clamp(0, 9999);
await _setCO2ConcentrationAlarmThreshold('$nextValue', emit);
}
Future<void> _setCO2ConcentrationAlarmThreshold(
String alarmThreshold,
Emitter<DeviceControlState> emit,
) async {
final newData = state.data.copyWith(
co2Concentration: state.data.co2Concentration.copyWith(
alarmThreshold: event.alarmThreshold,
alarmThreshold: alarmThreshold,
),
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(co2AlarmThreshold: event.alarmThreshold),
userSettings:
state.userSettings.copyWith(co2AlarmThreshold: alarmThreshold),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x31,
setValue: alarmThreshold,
errorPrefix: '二氧化碳浓度设定',
emit: emit,
);
}
// ==================== 护理等级控制事件处理 ====================
......@@ -402,7 +612,8 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(nebulizationTimeMinutes: event.minutes),
userSettings:
state.userSettings.copyWith(nebulizationTimeMinutes: event.minutes),
clearError: true,
));
// TODO: 发送设置到MCU
......@@ -422,7 +633,8 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(disinfectionTimeMinutes: event.minutes),
userSettings:
state.userSettings.copyWith(disinfectionTimeMinutes: event.minutes),
clearError: true,
));
// TODO: 发送设置到MCU
......@@ -495,7 +707,8 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
);
emit(state.copyWith(
data: newData,
userSettings: state.userSettings.copyWith(circulationModeValue: event.mode),
userSettings:
state.userSettings.copyWith(circulationModeValue: event.mode),
clearError: true,
));
// TODO: 发送设置到MCU
......@@ -619,6 +832,78 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
// TODO: 发送重置指令到MCU
}
Future<void> _writeControlValueToBluetooth({
required int identifier,
required String setValue,
required String errorPrefix,
required Emitter<DeviceControlState> emit,
int scale = 1,
}) async {
final command = _buildControlValueCommand(
identifier: identifier,
setValue: setValue,
scale: scale,
);
if (command == null) {
emit(state.copyWith(error: '$errorPrefix值无效: $setValue'));
return;
}
try {
if (!_bluetoothManager.isConnected) {
final boundDevice = await _storageService.getBoundBluetoothDevice();
final deviceId = boundDevice['deviceId'];
if (deviceId == null || deviceId.isEmpty) {
emit(state.copyWith(error: '未绑定蓝牙设备,无法发送$errorPrefix'));
return;
}
await _bluetoothManager.connectToDeviceId(deviceId);
}
await _bluetoothManager.writeData(command);
} catch (e) {
emit(state.copyWith(error: '$errorPrefix发送失败: $e'));
}
}
List<int>? _buildControlValueCommand({
required int identifier,
required String setValue,
int scale = 1,
}) {
final value = double.tryParse(setValue);
if (value == null) return null;
final payloadValue = (value * scale).round();
return [
0x68,
0x02,
0x02,
0x01,
identifier,
payloadValue & 0xFF,
(payloadValue >> 8) & 0xFF,
0x16,
];
}
String _formatTemperature(double value) {
if (value.isNaN) return '--';
final fixed = value.toStringAsFixed(1);
return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed;
}
String _formatInt(int value) {
return value < 0 ? '--' : '$value';
}
@override
Future<void> close() async {
await _mcuReportSub.cancel();
await _bluetoothDataSub.cancel();
return super.close();
}
// ==================== 便捷方法 - 供外部调用 ====================
/// [MCU调用] 更新设备SN
......@@ -628,85 +913,127 @@ class DeviceControlBloc extends HydratedBloc<DeviceControlEvent, DeviceControlSt
void setSn(String? sn) => add(DeviceControlSnChanged(sn));
/// [MCU调用] 更新舱内温度当前值
void updateCabinTempCurrentValue(String value) => add(DeviceControlCabinTempCurrentValueFetched(value));
void updateCabinTempCurrentValue(String value) =>
add(DeviceControlCabinTempCurrentValueFetched(value));
/// [用户调用] 设置舱内温度设定值
void setCabinTempSetValue(String value) => add(DeviceControlCabinTempSetValueChanged(value));
void setCabinTempSetValue(String value) =>
add(DeviceControlCabinTempSetValueChanged(value));
/// [用户调用] 调整舱内温度设定值
void adjustCabinTempSetValue(int step) =>
add(DeviceControlCabinTempSetValueAdjusted(step));
/// [用户调用] 设置舱内温度开关
void setCabinTempSwitch(bool isOn) => add(DeviceControlCabinTempSwitchChanged(isOn));
void setCabinTempSwitch(bool isOn) =>
add(DeviceControlCabinTempSwitchChanged(isOn));
/// [MCU调用] 更新舱内湿度当前值
void updateCabinHumidityCurrentValue(String value) => add(DeviceControlCabinHumidityCurrentValueFetched(value));
void updateCabinHumidityCurrentValue(String value) =>
add(DeviceControlCabinHumidityCurrentValueFetched(value));
/// [用户调用] 设置舱内湿度设定值
void setCabinHumiditySetValue(String value) => add(DeviceControlCabinHumiditySetValueChanged(value));
void setCabinHumiditySetValue(String value) =>
add(DeviceControlCabinHumiditySetValueChanged(value));
/// [用户调用] 调整舱内湿度设定值
void adjustCabinHumiditySetValue(int step) =>
add(DeviceControlCabinHumiditySetValueAdjusted(step));
/// [用户调用] 设置舱内湿度开关
void setCabinHumiditySwitch(bool isOn) => add(DeviceControlCabinHumiditySwitchChanged(isOn));
void setCabinHumiditySwitch(bool isOn) =>
add(DeviceControlCabinHumiditySwitchChanged(isOn));
/// [MCU调用] 更新氧气浓度当前值
void updateOxygenConcentrationCurrentValue(String value) => add(DeviceControlOxygenConcentrationCurrentValueFetched(value));
void updateOxygenConcentrationCurrentValue(String value) =>
add(DeviceControlOxygenConcentrationCurrentValueFetched(value));
/// [用户调用] 设置氧气浓度设定值
void setOxygenConcentrationSetValue(String value) => add(DeviceControlOxygenConcentrationSetValueChanged(value));
void setOxygenConcentrationSetValue(String value) =>
add(DeviceControlOxygenConcentrationSetValueChanged(value));
/// [用户调用] 调整氧气浓度设定值
void adjustOxygenConcentrationSetValue(int step) =>
add(DeviceControlOxygenConcentrationSetValueAdjusted(step));
/// [用户调用] 设置氧气浓度开关
void setOxygenConcentrationSwitch(bool isOn) => add(DeviceControlOxygenConcentrationSwitchChanged(isOn));
void setOxygenConcentrationSwitch(bool isOn) =>
add(DeviceControlOxygenConcentrationSwitchChanged(isOn));
/// [MCU调用] 更新二氧化碳浓度当前值
void updateCO2ConcentrationCurrentValue(String value) => add(DeviceControlCO2ConcentrationCurrentValueFetched(value));
void updateCO2ConcentrationCurrentValue(String value) =>
add(DeviceControlCO2ConcentrationCurrentValueFetched(value));
/// [用户调用] 设置二氧化碳浓度告警阈值
void setCO2ConcentrationAlarmThreshold(String value) => add(DeviceControlCO2ConcentrationAlarmThresholdChanged(value));
void setCO2ConcentrationAlarmThreshold(String value) =>
add(DeviceControlCO2ConcentrationAlarmThresholdChanged(value));
/// [用户调用] 调整二氧化碳浓度告警阈值
void adjustCO2ConcentrationAlarmThreshold(int step) =>
add(DeviceControlCO2ConcentrationAlarmThresholdAdjusted(step));
/// [用户调用] 设置护理等级
void setCareLevel(CareLevel level) => add(DeviceControlCareLevelChanged(level));
void setCareLevel(CareLevel level) =>
add(DeviceControlCareLevelChanged(level));
/// [用户调用] 设置新风模式
void setFreshAirMode(FreshAirMode mode) => add(DeviceControlFreshAirModeChanged(mode));
void setFreshAirMode(FreshAirMode mode) =>
add(DeviceControlFreshAirModeChanged(mode));
/// [用户调用] 设置风速模式
void setWindSpeedMode(WindSpeedMode mode) => add(DeviceControlWindSpeedModeChanged(mode));
void setWindSpeedMode(WindSpeedMode mode) =>
add(DeviceControlWindSpeedModeChanged(mode));
/// [用户调用] 设置雾化时间
void setNebulizationTime(String minutes) => add(DeviceControlNebulizationTimeChanged(minutes));
void setNebulizationTime(String minutes) =>
add(DeviceControlNebulizationTimeChanged(minutes));
/// [用户调用] 设置消毒时间
void setDisinfectionTime(String minutes) => add(DeviceControlDisinfectionTimeChanged(minutes));
void setDisinfectionTime(String minutes) =>
add(DeviceControlDisinfectionTimeChanged(minutes));
/// [用户调用] 设置开放供氧开关
void setOpenOxygenSwitch(bool isOn) => add(DeviceControlOpenOxygenSwitchChanged(isOn));
void setOpenOxygenSwitch(bool isOn) =>
add(DeviceControlOpenOxygenSwitchChanged(isOn));
/// [MCU调用] 更新供氧计时
void updateOxygenTimer(String hours, String minutes, String seconds) => add(DeviceControlOxygenTimerUpdated(hours, minutes, seconds));
void updateOxygenTimer(String hours, String minutes, String seconds) =>
add(DeviceControlOxygenTimerUpdated(hours, minutes, seconds));
/// [用户调用] 清零供氧计时
void clearOxygenTimer() => add(const DeviceControlOxygenTimerCleared());
/// [用户调用] 设置循环模式
void setCirculationMode(CirculationMode mode) => add(DeviceControlCirculationModeChanged(mode));
void setCirculationMode(CirculationMode mode) =>
add(DeviceControlCirculationModeChanged(mode));
/// [用户调用] 设置检查灯开关
void setCheckLightSwitch(bool isOn) => add(DeviceControlCheckLightSwitchChanged(isOn));
void setCheckLightSwitch(bool isOn) =>
add(DeviceControlCheckLightSwitchChanged(isOn));
/// [用户调用] 设置照明灯开关
void setIlluminationLightSwitch(bool isOn) => add(DeviceControlIlluminationLightSwitchChanged(isOn));
void setIlluminationLightSwitch(bool isOn) =>
add(DeviceControlIlluminationLightSwitchChanged(isOn));
/// [用户调用] 设置蓝光灯开关
void setBlueLightSwitch(bool isOn) => add(DeviceControlBlueLightSwitchChanged(isOn));
void setBlueLightSwitch(bool isOn) =>
add(DeviceControlBlueLightSwitchChanged(isOn));
/// [用户调用] 设置红光灯开关
void setRedLightSwitch(bool isOn) => add(DeviceControlRedLightSwitchChanged(isOn));
void setRedLightSwitch(bool isOn) =>
add(DeviceControlRedLightSwitchChanged(isOn));
/// [MCU调用] 更新总运行时长
void updateTotalRunTime(String time) => add(DeviceControlTotalRunTimeUpdated(time));
void updateTotalRunTime(String time) =>
add(DeviceControlTotalRunTimeUpdated(time));
/// [MCU调用] 更新环境温度
void updateEnvironmentTemp(String temp) => add(DeviceControlEnvironmentTempUpdated(temp));
void updateEnvironmentTemp(String temp) =>
add(DeviceControlEnvironmentTempUpdated(temp));
/// [MCU调用] 更新总制氧时长
void updateTotalOxygenTime(String time) => add(DeviceControlTotalOxygenTimeUpdated(time));
void updateTotalOxygenTime(String time) =>
add(DeviceControlTotalOxygenTimeUpdated(time));
/// [用户调用] 重置设备设置
void resetSettings() => add(const DeviceControlResetSettings());
......
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_control_bo.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
/// 用途:设备控制事件基类
abstract class DeviceControlEvent extends Equatable {
......@@ -26,6 +27,16 @@ class DeviceControlDataUpdated extends DeviceControlEvent {
List<Object?> get props => [data];
}
/// MCU上报数据同步到设备控制
class DeviceControlMcuReportFetched extends DeviceControlEvent {
final McuReport report;
const DeviceControlMcuReportFetched(this.report);
@override
List<Object?> get props => [report];
}
// ==================== 设备SN事件 ====================
/// 获取设备SN(从MCU获取)
......@@ -70,6 +81,16 @@ class DeviceControlCabinTempSetValueChanged extends DeviceControlEvent {
List<Object?> get props => [setValue];
}
/// 调整舱内温度设定值(用户点击 -/+,发送给MCU)
class DeviceControlCabinTempSetValueAdjusted extends DeviceControlEvent {
final int step;
const DeviceControlCabinTempSetValueAdjusted(this.step);
@override
List<Object?> get props => [step];
}
/// 设置舱内温度开关
class DeviceControlCabinTempSwitchChanged extends DeviceControlEvent {
final bool isOn;
......@@ -102,6 +123,16 @@ class DeviceControlCabinHumiditySetValueChanged extends DeviceControlEvent {
List<Object?> get props => [setValue];
}
/// 调整舱内湿度设定值(用户点击 -/+,发送给MCU)
class DeviceControlCabinHumiditySetValueAdjusted extends DeviceControlEvent {
final int step;
const DeviceControlCabinHumiditySetValueAdjusted(this.step);
@override
List<Object?> get props => [step];
}
/// 设置舱内湿度开关
class DeviceControlCabinHumiditySwitchChanged extends DeviceControlEvent {
final bool isOn;
......@@ -115,7 +146,8 @@ class DeviceControlCabinHumiditySwitchChanged extends DeviceControlEvent {
// ==================== 氧气浓度控制事件 ====================
/// 获取氧气浓度当前值(从MCU获取)
class DeviceControlOxygenConcentrationCurrentValueFetched extends DeviceControlEvent {
class DeviceControlOxygenConcentrationCurrentValueFetched
extends DeviceControlEvent {
final String currentValue;
const DeviceControlOxygenConcentrationCurrentValueFetched(this.currentValue);
......@@ -125,7 +157,8 @@ class DeviceControlOxygenConcentrationCurrentValueFetched extends DeviceControlE
}
/// 设置氧气浓度设定值(用户配置,发送给MCU)
class DeviceControlOxygenConcentrationSetValueChanged extends DeviceControlEvent {
class DeviceControlOxygenConcentrationSetValueChanged
extends DeviceControlEvent {
final String setValue;
const DeviceControlOxygenConcentrationSetValueChanged(this.setValue);
......@@ -134,6 +167,17 @@ class DeviceControlOxygenConcentrationSetValueChanged extends DeviceControlEvent
List<Object?> get props => [setValue];
}
/// 调整氧气浓度设定值(用户点击 -/+,发送给MCU)
class DeviceControlOxygenConcentrationSetValueAdjusted
extends DeviceControlEvent {
final int step;
const DeviceControlOxygenConcentrationSetValueAdjusted(this.step);
@override
List<Object?> get props => [step];
}
/// 设置氧气浓度开关
class DeviceControlOxygenConcentrationSwitchChanged extends DeviceControlEvent {
final bool isOn;
......@@ -147,7 +191,8 @@ class DeviceControlOxygenConcentrationSwitchChanged extends DeviceControlEvent {
// ==================== 二氧化碳浓度控制事件 ====================
/// 获取二氧化碳浓度当前值(从MCU获取)
class DeviceControlCO2ConcentrationCurrentValueFetched extends DeviceControlEvent {
class DeviceControlCO2ConcentrationCurrentValueFetched
extends DeviceControlEvent {
final String currentValue;
const DeviceControlCO2ConcentrationCurrentValueFetched(this.currentValue);
......@@ -157,7 +202,8 @@ class DeviceControlCO2ConcentrationCurrentValueFetched extends DeviceControlEven
}
/// 设置二氧化碳浓度告警阈值(用户配置,发送给MCU)
class DeviceControlCO2ConcentrationAlarmThresholdChanged extends DeviceControlEvent {
class DeviceControlCO2ConcentrationAlarmThresholdChanged
extends DeviceControlEvent {
final String alarmThreshold;
const DeviceControlCO2ConcentrationAlarmThresholdChanged(this.alarmThreshold);
......@@ -166,6 +212,17 @@ class DeviceControlCO2ConcentrationAlarmThresholdChanged extends DeviceControlEv
List<Object?> get props => [alarmThreshold];
}
/// 调整二氧化碳浓度设定值(用户点击 -/+,发送给MCU)
class DeviceControlCO2ConcentrationAlarmThresholdAdjusted
extends DeviceControlEvent {
final int step;
const DeviceControlCO2ConcentrationAlarmThresholdAdjusted(this.step);
@override
List<Object?> get props => [step];
}
// ==================== 护理等级控制事件 ====================
/// 设置护理等级(用户配置,发送给MCU)
......
......@@ -8,25 +8,25 @@ const DeviceControlBO _defaultDeviceControlBO = DeviceControlBO(
environmentTemp: '25.0℃',
totalOxygenTime: '0h',
cabinTemp: TemperatureControlBO(
currentValue: '24',
currentValue: '--',
setValue: '24',
unit: '℃',
isOn: false,
),
cabinHumidity: HumidityControlBO(
currentValue: '24',
currentValue: '--',
setValue: '50',
unit: '%',
isOn: false,
),
oxygenConcentration: OxygenConcentrationBO(
currentValue: '24',
currentValue: '--',
setValue: '90',
unit: '%',
isOn: true,
),
co2Concentration: CO2ConcentrationBO(
currentValue: '2400',
currentValue: '--',
alarmThreshold: '4000',
unit: 'PPM',
),
......@@ -106,7 +106,15 @@ class DeviceControlState extends Equatable {
/// 从持久化存储中恢复状态(仅恢复用户设置,MCU值使用默认值)
factory DeviceControlState.fromJson(Map<String, dynamic> json) {
final userSettings = DeviceControlUserSettings.fromJson(json);
final data = userSettings.applyTo(_defaultDeviceControlBO);
final restoredData = userSettings.applyTo(_defaultDeviceControlBO);
final data = restoredData.copyWith(
cabinTemp: restoredData.cabinTemp.copyWith(currentValue: '--'),
cabinHumidity: restoredData.cabinHumidity.copyWith(currentValue: '--'),
oxygenConcentration:
restoredData.oxygenConcentration.copyWith(currentValue: '--'),
co2Concentration:
restoredData.co2Concentration.copyWith(currentValue: '--'),
);
return DeviceControlState(
data: data,
userSettings: userSettings,
......
......@@ -23,10 +23,12 @@ class BluetoothReadModel extends Equatable {
factory BluetoothReadModel.fromMcuReport(McuReport report) {
return BluetoothReadModel(
sn: report.sn.trim().isEmpty ? null : report.sn.trim(),
mainTemperature: report.mainTemperature,
humidity: report.humidity,
oxygenConcentration: report.oxygenConcentration,
co2Measurement: report.co2Measurement,
mainTemperature:
report.mainTemperature.isNaN ? null : report.mainTemperature,
humidity: report.humidity < 0 ? null : report.humidity,
oxygenConcentration:
report.oxygenConcentration < 0 ? null : report.oxygenConcentration,
co2Measurement: report.co2Measurement < 0 ? null : report.co2Measurement,
updatedAt: DateTime.now(),
);
}
......
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:laki_icu_app/utils/logger.dart';
typedef BluetoothPayloadParser<T> = T? Function(List<int> bytes, String rawHex);
final BleBluetoothManager appBluetoothManager = BleBluetoothManager();
enum BluetoothConnectionStatus {
disconnected,
connecting,
......@@ -125,6 +129,7 @@ class BleBluetoothManager<T> {
await _ble.initialize();
final status = _ble.status;
debugPrint('🔥🔥🔥 BLE_CONNECT_DEBUG ensure_ready status=$status');
if (status == BleStatus.ready) {
return true;
}
......@@ -135,10 +140,14 @@ class BleBluetoothManager<T> {
.timeout(const Duration(seconds: 2), onTimeout: () => status);
if (nextStatus != BleStatus.ready) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG ensure_ready_failed nextStatus=$nextStatus');
_notifyMessage('蓝牙未就绪或未授权: $nextStatus');
return false;
}
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG ensure_ready_success nextStatus=$nextStatus');
return true;
}
......@@ -243,12 +252,18 @@ class BleBluetoothManager<T> {
}
Future<void> connectToDeviceId(String deviceId) async {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG connect_to_device_id start deviceId=$deviceId');
if (deviceId.isEmpty) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG connect_to_device_id empty_device_id');
_notifyMessage('无效的蓝牙设备地址');
return;
}
if (!await ensureBluetoothReady()) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG connect_to_device_id bluetooth_not_ready');
return;
}
......@@ -267,6 +282,7 @@ class BleBluetoothManager<T> {
_setStatus(BluetoothConnectionStatus.connecting);
_notifyMessage('正在连接蓝牙设备: $deviceId');
debugPrint('🔥🔥🔥 BLE_CONNECT_DEBUG connecting deviceId=$deviceId');
final completer = Completer<void>();
_connectionSubscription = _ble
......@@ -282,10 +298,14 @@ class BleBluetoothManager<T> {
switch (update.connectionState) {
case DeviceConnectionState.connecting:
_setStatus(BluetoothConnectionStatus.connecting);
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG state_connecting deviceId=$deviceId');
break;
case DeviceConnectionState.connected:
_setStatus(BluetoothConnectionStatus.connectedReady);
_notifyMessage('蓝牙连接成功: $deviceId');
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG state_connected deviceId=$deviceId');
await _enableNotifications(deviceId);
activateDataSource();
if (!completer.isCompleted) {
......@@ -294,10 +314,16 @@ class BleBluetoothManager<T> {
break;
case DeviceConnectionState.disconnecting:
_setStatus(BluetoothConnectionStatus.disconnected);
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG state_disconnecting deviceId=$deviceId');
break;
case DeviceConnectionState.disconnected:
_setStatus(BluetoothConnectionStatus.disconnected);
_notifyMessage('蓝牙连接断开: $deviceId');
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG state_disconnected deviceId=$deviceId '
'failure=${update.failure}',
);
_clearCharacteristics();
if (!completer.isCompleted && update.failure != null) {
completer.completeError(update.failure!);
......@@ -308,6 +334,8 @@ class BleBluetoothManager<T> {
onError: (Object error) {
_setStatus(BluetoothConnectionStatus.disconnected);
_notifyMessage('蓝牙连接失败: $error');
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG connect_error deviceId=$deviceId error=$error');
if (!completer.isCompleted) {
completer.completeError(error);
}
......@@ -317,6 +345,8 @@ class BleBluetoothManager<T> {
try {
await completer.future.timeout(connectionTimeout);
} catch (_) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG connect_timeout_or_error deviceId=$deviceId');
await disconnect();
rethrow;
}
......@@ -327,6 +357,8 @@ class BleBluetoothManager<T> {
_status == BluetoothConnectionStatus.active) {
_setStatus(BluetoothConnectionStatus.active);
_notifyMessage('蓝牙数据源已激活');
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG data_source_active deviceId=$_connectedDeviceId');
}
}
......@@ -395,10 +427,16 @@ class BleBluetoothManager<T> {
Future<void> _enableNotifications(String deviceId) async {
final characteristic = _notifyCharacteristic;
if (characteristic == null) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG notify_characteristic_null deviceId=$deviceId');
throw StateError('接收特征值为空');
}
await _notifySubscription?.cancel();
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG notify_subscribe_start deviceId=$deviceId '
'service=$serviceUuid characteristic=$notifyCharacteristicUuid',
);
_notifySubscription = _ble.subscribeToCharacteristic(characteristic).listen(
(bytes) {
if (!isActive || bytes.isEmpty) {
......@@ -406,6 +444,10 @@ class BleBluetoothManager<T> {
}
final rawHex = bytesToHex(bytes);
debugPrint(
'🔥🔥🔥 BLE_READ_DEBUG manager_notify deviceId=$deviceId len=${bytes.length} hex=$rawHex');
Log.warn('🔥🔥🔥 BLE_READ_DEBUG manager_notify deviceId=$deviceId '
'len=${bytes.length} hex=$rawHex');
final parsedData = _parser?.call(bytes, rawHex);
_dataController.add(
BluetoothDataPacket<T>(
......@@ -416,8 +458,14 @@ class BleBluetoothManager<T> {
),
);
},
onError: (Object error) => _notifyMessage('接收蓝牙数据失败: $error'),
onError: (Object error) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG notify_error deviceId=$deviceId error=$error');
_notifyMessage('接收蓝牙数据失败: $error');
},
);
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG notify_subscribe_done deviceId=$deviceId');
}
void _clearCharacteristics() {
......@@ -427,6 +475,10 @@ class BleBluetoothManager<T> {
void _setStatus(BluetoothConnectionStatus status) {
_status = status;
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG manager_status status=$status '
'deviceId=$_connectedDeviceId',
);
if (!_stateController.isClosed) {
_stateController.add(status);
}
......
import 'dart:convert';
const int mcuReportBytes = 145;
const int mcuReportHeaderBytes = 5;
const int mcuReportTailBytes = 3;
class HexReader {
HexReader(String hexData) {
......@@ -37,10 +39,20 @@ class HexReader {
return int.parse(readHex(2), radix: 16);
}
int readUInt16LE() {
final low = readUInt8();
final high = readUInt8();
return low | (high << 8);
}
double readTenths() {
return readUInt16() / 10;
}
double readTenthsLE() {
return readUInt16LE() / 10;
}
int readHighByte() {
return readUInt16() >> 8;
}
......@@ -330,85 +342,104 @@ class McuReport {
class McuReportParser {
static McuReport parseHex(String hexData) {
final data = HexReader(hexData);
if (data.remainingBytes < mcuReportBytes) {
if (data.remainingBytes < mcuReportHeaderBytes) {
throw RangeError(
'完整 MCU 上报需要 $mcuReportBytes 字节,当前只有 ${data.remainingBytes} 字节',
'MCU 上报至少需要 $mcuReportHeaderBytes 字节头,当前只有 ${data.remainingBytes} 字节',
);
}
final head = data.readHex(2);
final direct = data.readHex(1);
final serviceNum = data.readHex(1);
final length = data.readUInt8();
final payloadBytes = data.remainingBytes >= length + mcuReportTailBytes
? length
: data.remainingBytes;
final payload =
payloadBytes > 0 ? HexReader(data.readHex(payloadBytes)) : null;
int remainingPayloadBytes() => payload?.remainingBytes ?? 0;
int u16() => remainingPayloadBytes() >= 2 ? payload!.readUInt16LE() : -1;
double tenths() =>
remainingPayloadBytes() >= 2 ? payload!.readTenthsLE() : double.nan;
int highByte() =>
remainingPayloadBytes() >= 2 ? payload!.readUInt16LE() >> 8 : -1;
String hex2() => remainingPayloadBytes() >= 2 ? payload!.readHex(2) : '';
String ascii(int bytes) =>
remainingPayloadBytes() >= bytes ? payload!.readAscii(bytes) : '';
return McuReport(
head: data.readHex(2),
direct: data.readHex(1),
serviceNum: data.readHex(1),
length: data.readUInt8(),
mainTemperature: data.readTenths(),
versionNumber: data.readTenths(),
ambientTemperature: data.readTenths(),
upperTemperature: data.readTenths(),
humidity: data.readUInt16(),
oxygenConcentration: data.readUInt16(),
temperatureSetting: data.readTenths(),
lowerTemperature: data.readTenths(),
humiditySetting: data.readUInt16(),
oxygenSetting: data.readUInt16(),
internalExternalCycleState: data.readUInt16(),
negativeIonSwitch: data.readUInt16(),
uvLamp: data.readUInt16(),
inspectionLamp: data.readUInt16(),
floodLight: data.readUInt16(),
o2ChangeSlope: data.readUInt16(),
rightAtomizerTreatmentTime: data.readUInt16(),
rightInfraredPhysiotherapyTime: data.readUInt16(),
tempS1: data.readHighByte() / 10,
tempH1: data.readHighByte(),
atomizerTreatmentTime: data.readUInt16(),
infraredPhysiotherapyTime: data.readHighByte(),
mainTempCorrect: data.readHighByte(),
auxiliaryTemperatureCorrect: data.readHighByte(),
o2CorrectStart: data.readHighByte(),
o2CorrectDiffer: data.readHighByte(),
humidityCorrect: data.readHighByte(),
oxygenCorrect: data.readHighByte(),
eTempCorrect: data.readHighByte(),
xyyModel: data.readHex(2),
unknown: data.readHex(2),
wifible: data.readHighByte(),
levelLight: data.readHighByte(),
openO2: data.readHighByte(),
airConditioner: data.readHighByte(),
co2Correct: data.readHighByte(),
co2Measurement: data.readUInt16(),
co2Setting: data.readUInt16(),
co2FastClearSwitch: data.readHighByte(),
openO2SettingMax: data.readHighByte(),
noOpenO2SettingMax: data.readHighByte(),
windSet: data.readHighByte(),
rangeData1: data.readHex(2),
rangeData2: data.readHex(2),
rangeData3: data.readHex(2),
co2Safety: data.readUInt16(),
co2SettingMin: data.readUInt16(),
blueLight: data.readHighByte(),
redLight: data.readHighByte(),
fcSwitch: data.readHighByte(),
o2SupplyTime: data.readUInt16(),
totalTime: data.readUInt16(),
ratioTemp2: data.readUInt8(),
ratioTemp1: data.readUInt8(),
sp: data.readHighByte(),
dp: data.readHighByte(),
mean: data.readHighByte(),
pulseRate: data.readHighByte(),
xyyStatus: data.readHighByte(),
xyyError1: data.readHighByte(),
sn: data.readAscii(10),
topTempCorrect: data.readHighByte(),
midTempCorrect: data.readHighByte(),
o2OpenCorrectStart: data.readHighByte(),
o2OpenCorrectStep: data.readHighByte(),
o2CorrectMax: data.readHighByte(),
o2OpenCorrectMax: data.readHighByte(),
parsedBytes: data.byteIndex,
head: head,
direct: direct,
serviceNum: serviceNum,
length: length,
mainTemperature: tenths(),
versionNumber: tenths(),
ambientTemperature: tenths(),
upperTemperature: tenths(),
humidity: u16(),
oxygenConcentration: u16(),
temperatureSetting: tenths(),
lowerTemperature: tenths(),
humiditySetting: u16(),
oxygenSetting: u16(),
internalExternalCycleState: u16(),
negativeIonSwitch: u16(),
uvLamp: u16(),
inspectionLamp: u16(),
floodLight: u16(),
o2ChangeSlope: u16(),
rightAtomizerTreatmentTime: u16(),
rightInfraredPhysiotherapyTime: u16(),
tempS1: highByte() / 10,
tempH1: highByte(),
atomizerTreatmentTime: u16(),
infraredPhysiotherapyTime: highByte(),
mainTempCorrect: highByte(),
auxiliaryTemperatureCorrect: highByte(),
o2CorrectStart: highByte(),
o2CorrectDiffer: highByte(),
humidityCorrect: highByte(),
oxygenCorrect: highByte(),
eTempCorrect: highByte(),
xyyModel: hex2(),
unknown: hex2(),
wifible: highByte(),
levelLight: highByte(),
openO2: highByte(),
airConditioner: highByte(),
co2Correct: highByte(),
co2Measurement: u16(),
co2Setting: u16(),
co2FastClearSwitch: highByte(),
openO2SettingMax: highByte(),
noOpenO2SettingMax: highByte(),
windSet: highByte(),
rangeData1: hex2(),
rangeData2: hex2(),
rangeData3: hex2(),
co2Safety: u16(),
co2SettingMin: u16(),
blueLight: highByte(),
redLight: highByte(),
fcSwitch: highByte(),
o2SupplyTime: u16(),
totalTime: u16(),
ratioTemp2: remainingPayloadBytes() >= 1 ? payload!.readUInt8() : -1,
ratioTemp1: remainingPayloadBytes() >= 1 ? payload!.readUInt8() : -1,
sp: highByte(),
dp: highByte(),
mean: highByte(),
pulseRate: highByte(),
xyyStatus: highByte(),
xyyError1: highByte(),
sn: ascii(10),
topTempCorrect: highByte(),
midTempCorrect: highByte(),
o2OpenCorrectStart: highByte(),
o2OpenCorrectStep: highByte(),
o2CorrectMax: highByte(),
o2OpenCorrectMax: highByte(),
parsedBytes: mcuReportHeaderBytes + (payload?.byteIndex ?? 0),
);
}
......@@ -427,6 +458,14 @@ class McuReportParser {
class McuReportFrameDecoder {
final List<int> _buffer = [];
int get bufferedBytes => _buffer.length;
String get bufferedHexPreview => _buffer
.take(24)
.map((item) => item.toRadixString(16).padLeft(2, '0'))
.join()
.toUpperCase();
List<McuReport> addBytes(List<int> bytes) {
if (bytes.isEmpty) {
return const [];
......@@ -435,10 +474,12 @@ class McuReportFrameDecoder {
_buffer.addAll(bytes);
final reports = <McuReport>[];
while (_buffer.length >= mcuReportBytes) {
final headIndex = _buffer.indexOf(0x68);
while (_buffer.length >= mcuReportHeaderBytes) {
final headIndex = _findReportHeadIndex();
if (headIndex < 0) {
_buffer.clear();
if (_buffer.length > 3) {
_buffer.removeRange(0, _buffer.length - 3);
}
break;
}
......@@ -446,23 +487,43 @@ class McuReportFrameDecoder {
_buffer.removeRange(0, headIndex);
}
if (_buffer.length < mcuReportBytes) {
if (_buffer.length < mcuReportHeaderBytes) {
break;
}
final frame = List<int>.from(_buffer.take(mcuReportBytes));
final payloadLength = _buffer[4];
final frameBytes =
mcuReportHeaderBytes + payloadLength + mcuReportTailBytes;
if (_buffer.length < frameBytes) {
reports.add(McuReportParser.parseBytes(List<int>.from(_buffer)));
break;
}
final frame = List<int>.from(_buffer.take(frameBytes));
if (frame.last != 0x16) {
_buffer.removeAt(0);
continue;
}
reports.add(McuReportParser.parseBytes(frame));
_buffer.removeRange(0, mcuReportBytes);
_buffer.removeRange(0, frameBytes);
}
return reports;
}
int _findReportHeadIndex() {
for (var index = 0; index <= _buffer.length - 4; index++) {
if (_buffer[index] == 0x68 &&
_buffer[index + 1] == 0x02 &&
_buffer[index + 2] == 0x01 &&
_buffer[index + 3] == 0x01) {
return index;
}
}
return -1;
}
void clear() {
_buffer.clear();
}
......
import 'dart:async';
import 'package:laki_icu_app/models/bo/bluetooth_read_model.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
class EventBus {
final StreamController<dynamic> _controller = StreamController.broadcast();
......@@ -41,3 +42,10 @@ class BluetoothReadInfoChangedEvent {
class BluetoothReadInfoClearedEvent {
const BluetoothReadInfoClearedEvent();
}
// MCU上报数据更新事件
class McuReportChangedEvent {
final McuReport report;
const McuReportChangedEvent(this.report);
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_bloc.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_event.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_state.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'device_control_common.dart';
......@@ -12,106 +16,164 @@ class DeviceControlMetricCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isTemperature = metric.label.contains('温度');
final isHumidity = metric.label.contains('湿度');
final isOxygen = metric.label.contains('氧气');
final isCo2 = metric.label.contains('二氧化碳') ||
metric.label.toUpperCase().contains('CO');
return DeviceControlPanelFrame(
active: isOxygen,
backgroundAsset: isOxygen ? assetMetricGreen : assetMetricBlue,
padding: EdgeInsets.fromLTRB(32.w, 26.h, 32.w, 28.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: DeviceControlPanelTitle(_title(metric.label)),
),
DeviceControlPill(
text: isOxygen
? 'ON'
: isCo2
? 'PPM'
: 'Off',
active: isOxygen,
),
],
),
const Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
return BlocBuilder<DeviceControlBloc, DeviceControlState>(
buildWhen: (previous, current) =>
previous.data.cabinTemp.setValue != current.data.cabinTemp.setValue ||
previous.data.cabinHumidity.setValue !=
current.data.cabinHumidity.setValue ||
previous.data.oxygenConcentration.setValue !=
current.data.oxygenConcentration.setValue ||
previous.data.co2Concentration.alarmThreshold !=
current.data.co2Concentration.alarmThreshold ||
previous.data.cabinTemp.currentValue !=
current.data.cabinTemp.currentValue ||
previous.data.cabinHumidity.currentValue !=
current.data.cabinHumidity.currentValue ||
previous.data.oxygenConcentration.currentValue !=
current.data.oxygenConcentration.currentValue ||
previous.data.co2Concentration.currentValue !=
current.data.co2Concentration.currentValue,
builder: (context, state) {
final currentValue = isTemperature
? state.data.cabinTemp.currentValue
: isCo2
? state.data.co2Concentration.currentValue
: isOxygen
? state.data.oxygenConcentration.currentValue
: isHumidity
? state.data.cabinHumidity.currentValue
: metric.value;
final setValue = isTemperature
? state.data.cabinTemp.setValue
: isCo2
? state.data.co2Concentration.alarmThreshold
: isOxygen
? state.data.oxygenConcentration.setValue
: isHumidity
? state.data.cabinHumidity.setValue
: '24';
final adjustEventBuilder = _adjustEventBuilder(
isTemperature: isTemperature,
isHumidity: isHumidity,
isOxygen: isOxygen,
isCo2: isCo2,
);
return DeviceControlPanelFrame(
active: isOxygen,
backgroundAsset: isOxygen ? assetMetricGreen : assetMetricBlue,
padding: EdgeInsets.fromLTRB(32.w, 26.h, 32.w, 28.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metric.value,
style: TextStyle(
color: Colors.white,
fontSize: isCo2 ? 60.sp : 82.sp,
height: 0.9,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 12.w),
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
isCo2 ? '' : metric.unit,
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
Row(
children: [
Expanded(
child: DeviceControlPanelTitle(_title(metric.label)),
),
),
DeviceControlPill(
text: isOxygen
? 'ON'
: isCo2
? 'PPM'
: 'Off',
active: isOxygen,
),
],
),
const Spacer(),
Image.asset(
deviceControlIconAsset(metric.label),
width: 72.w,
height: 72.w,
fit: BoxFit.contain,
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currentValue,
style: TextStyle(
color: Colors.white,
fontSize: isCo2 ? 60.sp : 82.sp,
height: 0.9,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 12.w),
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
isCo2 ? '' : metric.unit,
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
),
),
),
const Spacer(),
Image.asset(
deviceControlIconAsset(metric.label),
width: 72.w,
height: 72.w,
fit: BoxFit.contain,
),
],
),
],
),
Divider(color: Colors.white.withValues(alpha: 0.18), height: 28.h),
Text(
isCo2
? '告警阈值设定'
: metric.label.contains('温度')
? '温度设定'
: '湿度设定',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.78),
fontSize: 26.sp,
),
),
const Spacer(),
Row(
children: [
DeviceControlAdjustText('-', active: isOxygen || !isCo2),
const Spacer(),
Divider(
color: Colors.white.withValues(alpha: 0.18), height: 28.h),
Text(
isCo2
? '4000'
: isOxygen
? '90'
: metric.label.contains('湿度')
? '10'
: '24',
? '告警阈值设定'
: metric.label.contains('温度')
? '温度设定'
: '湿度设定',
style: TextStyle(
color: isCo2
? const Color(0xFFFFC400)
: const Color(0xFF00B7F4),
fontSize: 64.sp,
height: 1,
fontWeight: FontWeight.bold,
color: Colors.white.withValues(alpha: 0.78),
fontSize: 26.sp,
),
),
const Spacer(),
const DeviceControlAdjustText('+', active: true),
Row(
children: [
_MetricAdjustButton(
text: '-',
active: adjustEventBuilder != null,
onTap: adjustEventBuilder == null
? null
: () => context.read<DeviceControlBloc>().add(
adjustEventBuilder(false),
),
),
const Spacer(),
Text(
setValue,
style: TextStyle(
color: isCo2
? const Color(0xFFFFC400)
: const Color(0xFF00B7F4),
fontSize: 64.sp,
height: 1,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
_MetricAdjustButton(
text: '+',
active: adjustEventBuilder != null,
onTap: adjustEventBuilder == null
? null
: () => context.read<DeviceControlBloc>().add(
adjustEventBuilder(true),
),
),
],
),
],
),
],
),
);
},
);
}
......@@ -119,4 +181,53 @@ class DeviceControlMetricCard extends StatelessWidget {
if (label.contains('二氧化碳')) return '二氧化碳浓度';
return label;
}
static DeviceControlEvent Function(bool increase)? _adjustEventBuilder({
required bool isTemperature,
required bool isHumidity,
required bool isOxygen,
required bool isCo2,
}) {
if (isTemperature) {
return (increase) =>
DeviceControlCabinTempSetValueAdjusted(increase ? 1 : -1);
}
if (isHumidity) {
return (increase) =>
DeviceControlCabinHumiditySetValueAdjusted(increase ? 1 : -1);
}
if (isOxygen) {
return (increase) =>
DeviceControlOxygenConcentrationSetValueAdjusted(increase ? 1 : -1);
}
if (isCo2) {
return (increase) => DeviceControlCO2ConcentrationAlarmThresholdAdjusted(
increase ? 100 : -100);
}
return null;
}
}
class _MetricAdjustButton extends StatelessWidget {
const _MetricAdjustButton({
required this.text,
required this.active,
this.onTap,
});
final String text;
final bool active;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
child: DeviceControlAdjustText(text, active: active),
),
);
}
}
......@@ -10,6 +10,8 @@ import 'package:laki_icu_app/services/webrtc_service.dart';
import 'package:laki_icu_app/models/bo/bluetooth_read_model.dart';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:laki_icu_app/utils/event_bus.dart';
import 'package:laki_icu_app/utils/logger.dart';
import 'package:laki_icu_app/utils/mqtt/index.dart';
import 'package:laki_icu_app/utils/storage/storage_service.dart';
import 'monitoring_index_state.dart';
......@@ -24,6 +26,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final StorageService _storageService;
final BleBluetoothManager _bluetoothManager;
final McuReportFrameDecoder _mcuReportDecoder;
LakiMqttClient? _mqttClient;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
StreamSubscription<P2pServiceState>? _p2pStateSub;
......@@ -31,10 +34,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
StreamSubscription<BluetoothConnectionStatus>? _bluetoothStateSub;
StreamSubscription<String>? _bluetoothMessageSub;
StreamSubscription<BluetoothDataPacket>? _bluetoothDataSub;
StreamSubscription<McuReportChangedEvent>? _mcuReportChangedSub;
StreamSubscription<LakiMqttConnectionState>? _mqttStateSub;
StreamSubscription<LakiMqttMessage>? _mqttMessageSub;
StreamSubscription<String>? _mqttErrorSub;
Timer? _bluetoothReconnectTimer;
Timer? _mqttReconnectTimer;
bool _isAutoReconnectEnabled = false;
bool _isAutoConnectingBluetooth = false;
bool _isManualUnbindingBluetooth = false;
bool _isMqttReconnectEnabled = false;
bool _isMqttConnecting = false;
String? _mqttDeviceSn;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
......@@ -45,12 +56,13 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_webrtcService = WebrtcService(),
_p2pVideoService = P2pVideoService(),
_storageService = StorageService(),
_bluetoothManager = BleBluetoothManager(),
_bluetoothManager = appBluetoothManager,
_mcuReportDecoder = McuReportFrameDecoder(),
super(const MonitoringIndexState()) {
_listenWebrtcState();
_listenP2pState();
_listenBluetoothState();
_listenMcuReportChanged();
_loadBoundBluetoothDevice();
loadData();
}
......@@ -96,17 +108,42 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_bluetoothDataSub = _bluetoothManager.dataStream.listen((packet) async {
if (isClosed) return;
try {
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG index_packet len=${packet.bytes.length} '
'hex=${packet.rawHex}',
);
final reports = _mcuReportDecoder.addBytes(packet.bytes);
final latestReport = reports.isEmpty ? null : reports.last;
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG index_parse reports=${reports.length} '
'buffered=${_mcuReportDecoder.bufferedBytes} '
'bufferHex=${_mcuReportDecoder.bufferedHexPreview} '
'hasLatest=${latestReport != null}',
);
if (latestReport != null) {
Log.warn(
'🔥🔥🔥 BLE_READ_DEBUG index_report '
'frameLength=${latestReport.length} '
'temp=${latestReport.mainTemperature} '
'tempSet=${latestReport.temperatureSetting} '
'humidity=${latestReport.humidity} '
'humiditySet=${latestReport.humiditySetting} '
'o2=${latestReport.oxygenConcentration} '
'o2Set=${latestReport.oxygenSetting} '
'co2=${latestReport.co2Measurement} '
'co2Set=${latestReport.co2Setting} '
'sn=${latestReport.sn.isEmpty ? '<EMPTY>' : latestReport.sn}',
);
final bluetoothReadInfo =
BluetoothReadModel.fromMcuReport(latestReport);
if (bluetoothReadInfo.hasSn) {
await _storageService.saveBluetoothReadSn(bluetoothReadInfo.sn!);
await _connectMqttForDeviceSn(bluetoothReadInfo.sn!);
}
eventBus.emit(
BluetoothReadInfoChangedEvent(bluetoothReadInfo),
);
_publishMqttMonitorReport(latestReport);
if (isClosed) return;
}
emit(state.copyWith(
......@@ -128,17 +165,35 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
});
}
void _listenMcuReportChanged() {
_mcuReportChangedSub = eventBus.on<McuReportChangedEvent>().listen((event) {
if (isClosed) return;
emit(state.copyWith(
latestMcuReport: event.report,
metrics: _metricsFromBluetoothReport(event.report),
));
});
}
Future<void> _loadBoundBluetoothDevice() async {
final boundDevice = await _storageService.getBoundBluetoothDevice();
if (isClosed) return;
final deviceId = boundDevice['deviceId'];
await _storageService.getBluetoothReadInfo();
final bluetoothReadInfo = await _storageService.getBluetoothReadInfo();
if (isClosed) return;
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG load_bound_device '
'deviceId=$deviceId deviceName=${boundDevice['deviceName']} '
'hasSn=${bluetoothReadInfo.hasSn}',
);
emit(state.copyWith(
boundBluetoothDeviceId:
deviceId == null || deviceId.isEmpty ? null : deviceId,
boundBluetoothDeviceName: boundDevice['deviceName'],
));
if (bluetoothReadInfo.hasSn) {
_connectMqttForDeviceSn(bluetoothReadInfo.sn!);
}
if (deviceId != null && deviceId.isNotEmpty) {
_isAutoReconnectEnabled = true;
_connectBoundBluetoothDevice();
......@@ -359,12 +414,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
await _bluetoothManager.disconnect();
await _storageService.deleteBoundBluetoothDevice();
await _storageService.deleteBluetoothReadInfo();
_isMqttReconnectEnabled = false;
_mqttDeviceSn = null;
await _disposeMqttClient();
eventBus.emit(const BluetoothReadInfoClearedEvent());
_mcuReportDecoder.clear();
if (isClosed) return;
emit(state.copyWith(
clearBoundBluetoothDevice: true,
clearLatestMcuReport: true,
clearMqttDeviceSn: true,
mqttConnectionState: LakiMqttConnectionState.disconnected,
mqttMessage: 'MQTT 已断开',
bluetoothMessage: '蓝牙设备已解绑',
latestBluetoothRawHex: '',
));
......@@ -406,13 +467,23 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
if (!isClosed) {
emit(state.copyWith(bluetoothMessage: '正在自动连接蓝牙设备'));
}
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG auto_connect_start '
'deviceId=$deviceId isConnected=${_bluetoothManager.isConnected}',
);
try {
await _bluetoothManager.connectToDeviceId(deviceId);
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG auto_connect_success '
'deviceId=$deviceId isConnected=${_bluetoothManager.isConnected}',
);
if (!isClosed) {
emit(state.copyWith(bluetoothMessage: '蓝牙设备已自动连接'));
}
} catch (e) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG auto_connect_error deviceId=$deviceId error=$e');
if (!isClosed) {
emit(state.copyWith(bluetoothMessage: '蓝牙自动连接失败: $e'));
}
......@@ -429,26 +500,168 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_bluetoothReconnectTimer = null;
}
// ==================== MQTT 自动连接/上报 ====================
Future<void> _connectMqttForDeviceSn(String deviceSn) async {
if (deviceSn.isEmpty || isClosed) return;
if (_isMqttConnecting) return;
if (_mqttDeviceSn == deviceSn && (_mqttClient?.isConnected ?? false)) {
return;
}
_isMqttReconnectEnabled = true;
_isMqttConnecting = true;
_cancelMqttReconnect();
var shouldScheduleReconnect = false;
try {
if (_mqttDeviceSn != deviceSn) {
await _disposeMqttClient();
_mqttDeviceSn = deviceSn;
_mqttClient = LakiMqttClient(
config: LakiMqttConfig.tencentTdmq(
deviceSn: deviceSn,
logging: kDebugMode,
),
);
_listenMqttState(_mqttClient!);
}
if (!isClosed) {
emit(state.copyWith(
mqttConnectionState: LakiMqttConnectionState.connecting,
mqttMessage: '正在连接 MQTT: $deviceSn',
mqttDeviceSn: deviceSn,
));
}
await _mqttClient!.connect();
_mqttClient!
..subscribeIcuCommands(deviceSn)
..subscribePetBind(deviceSn);
if (!isClosed) {
emit(state.copyWith(
mqttConnectionState: LakiMqttConnectionState.connected,
mqttMessage: 'MQTT 已连接并订阅: $deviceSn',
mqttDeviceSn: deviceSn,
));
}
} catch (e) {
shouldScheduleReconnect = true;
if (!isClosed) {
emit(state.copyWith(
mqttConnectionState: LakiMqttConnectionState.failed,
mqttMessage: 'MQTT 自动连接失败: $e',
mqttDeviceSn: deviceSn,
));
}
} finally {
_isMqttConnecting = false;
if (shouldScheduleReconnect) {
_scheduleMqttReconnect();
}
}
}
void _listenMqttState(LakiMqttClient client) {
_mqttStateSub = client.stateStream.listen((status) {
if (isClosed) return;
emit(state.copyWith(mqttConnectionState: status));
if (status == LakiMqttConnectionState.disconnected &&
_isMqttReconnectEnabled) {
_scheduleMqttReconnect();
}
});
_mqttMessageSub = client.messageStream.listen((message) {
if (isClosed) return;
emit(state.copyWith(
mqttMessage: 'MQTT 收到 ${message.topic}: ${message.payload}',
));
});
_mqttErrorSub = client.errorStream.listen((message) {
if (isClosed) return;
emit(state.copyWith(mqttMessage: message));
});
}
void _publishMqttMonitorReport(McuReport report) {
final client = _mqttClient;
if (client == null || !client.isConnected || report.sn.isEmpty) {
return;
}
try {
client.publishMonitorReport(report);
if (!isClosed) {
emit(state.copyWith(mqttMessage: 'MQTT 已上报: ${report.sn}'));
}
} catch (e) {
if (!isClosed) {
emit(state.copyWith(mqttMessage: 'MQTT 上报失败: $e'));
}
}
}
void _scheduleMqttReconnect() {
if (!_isMqttReconnectEnabled ||
_isMqttConnecting ||
_mqttDeviceSn == null ||
_mqttDeviceSn!.isEmpty) {
return;
}
_mqttReconnectTimer ??= Timer(
const Duration(seconds: 5),
() {
_mqttReconnectTimer = null;
final deviceSn = _mqttDeviceSn;
if (deviceSn != null && deviceSn.isNotEmpty) {
_connectMqttForDeviceSn(deviceSn);
}
},
);
}
void _cancelMqttReconnect() {
_mqttReconnectTimer?.cancel();
_mqttReconnectTimer = null;
}
Future<void> _disposeMqttClient() async {
_cancelMqttReconnect();
await _mqttStateSub?.cancel();
await _mqttMessageSub?.cancel();
await _mqttErrorSub?.cancel();
_mqttStateSub = null;
_mqttMessageSub = null;
_mqttErrorSub = null;
await _mqttClient?.dispose();
_mqttClient = null;
}
List<MonitoringMetricBO> _metricsFromBluetoothReport(McuReport report) {
final replacements = <String, MonitoringMetricBO>{
'舱内温度': _replaceMetricValue(
'舱内温度',
report.mainTemperature.toStringAsFixed(1),
_formatReportTemperature(report.mainTemperature),
fallbackUnit: '℃',
),
'舱内湿度': _replaceMetricValue(
'舱内湿度',
'${report.humidity}',
_formatReportInt(report.humidity),
fallbackUnit: 'RH',
),
'氧气浓度': _replaceMetricValue(
'氧气浓度',
'${report.oxygenConcentration}',
_formatReportInt(report.oxygenConcentration),
fallbackUnit: '%',
),
'二氧化碳': _replaceMetricValue(
'二氧化碳',
'${report.co2Measurement}',
_formatReportInt(report.co2Measurement),
fallbackUnit: 'ppm',
),
};
......@@ -490,6 +703,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
);
}
String _formatReportTemperature(double value) {
if (value.isNaN) return '--';
final fixed = value.toStringAsFixed(1);
return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed;
}
String _formatReportInt(int value) {
return value < 0 ? '--' : '$value';
}
/// 清空宠物信息(出舱)
void clearPatientInfo() {
if (isClosed) return;
......@@ -499,7 +722,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
/// 设置测试宠物信息(绑定宠物)
void setTestPatientInfo() {
if (isClosed) return;
final testPatientInfo = PatientInfoBO(
const testPatientInfo = PatientInfoBO(
name: '妮蔻',
type: '猫',
phone: '130-1111-3333',
......@@ -522,14 +745,20 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
@override
Future<void> close() {
_cancelBluetoothReconnect();
_isMqttReconnectEnabled = false;
_cancelMqttReconnect();
_webrtcStateSub?.cancel();
_p2pStateSub?.cancel();
_bluetoothScanSub?.cancel();
_bluetoothStateSub?.cancel();
_bluetoothMessageSub?.cancel();
_bluetoothDataSub?.cancel();
_mcuReportChangedSub?.cancel();
_webrtcService.dispose();
_p2pVideoService.dispose();
return _bluetoothManager.release().then((_) => super.close());
return Future.wait([
_disposeMqttClient(),
_bluetoothManager.release(),
]).then((_) => super.close());
}
}
......@@ -3,6 +3,7 @@ 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';
import 'package:laki_icu_app/utils/mqtt/index.dart';
enum MonitoringIndexStatus {
initial,
......@@ -61,6 +62,15 @@ class MonitoringIndexState extends Equatable {
/// 最近解析出的 MCU 上报数据
final McuReport? latestMcuReport;
/// MQTT 连接状态
final LakiMqttConnectionState mqttConnectionState;
/// MQTT 状态提示
final String? mqttMessage;
/// 当前 MQTT 连接使用的设备 SN
final String? mqttDeviceSn;
const MonitoringIndexState({
this.status = MonitoringIndexStatus.initial,
this.isLoading = false,
......@@ -82,6 +92,9 @@ class MonitoringIndexState extends Equatable {
this.bluetoothMessage,
this.latestBluetoothRawHex,
this.latestMcuReport,
this.mqttConnectionState = LakiMqttConnectionState.disconnected,
this.mqttMessage,
this.mqttDeviceSn,
});
MonitoringIndexState copyWith({
......@@ -105,9 +118,13 @@ class MonitoringIndexState extends Equatable {
String? bluetoothMessage,
String? latestBluetoothRawHex,
McuReport? latestMcuReport,
LakiMqttConnectionState? mqttConnectionState,
String? mqttMessage,
String? mqttDeviceSn,
bool clearBoundBluetoothDevice = false,
bool clearLatestMcuReport = false,
bool clearPatientInfo = false,
bool clearMqttDeviceSn = false,
}) {
return MonitoringIndexState(
status: status ?? this.status,
......@@ -138,6 +155,10 @@ class MonitoringIndexState extends Equatable {
latestBluetoothRawHex ?? this.latestBluetoothRawHex,
latestMcuReport:
clearLatestMcuReport ? null : latestMcuReport ?? this.latestMcuReport,
mqttConnectionState: mqttConnectionState ?? this.mqttConnectionState,
mqttMessage: mqttMessage ?? this.mqttMessage,
mqttDeviceSn:
clearMqttDeviceSn ? null : mqttDeviceSn ?? this.mqttDeviceSn,
);
}
......@@ -163,5 +184,8 @@ class MonitoringIndexState extends Equatable {
bluetoothMessage,
latestBluetoothRawHex,
latestMcuReport,
mqttConnectionState,
mqttMessage,
mqttDeviceSn,
];
}
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