绑定设备+覆盖模式+控制显示bug+在线更新

This commit is contained in:
2026-09-01 08:59:20 +08:00
parent 0fa67a127b
commit 61bce2c1cf
49 changed files with 2424 additions and 720 deletions

View File

@@ -7,10 +7,12 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status
import 'package:maibu_satabot_v2/features/devices/domain/entities/gps_entity.dart';
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
import 'package:maibu_satabot_v2/core/network/mqtt/domain/repositories/task_message_repository.dart';
import 'package:maibu_satabot_v2/core/network/mqtt/domain/repositories/mower_realtime_repository.dart';
import 'package:maibu_satabot_v2/core/network/mqtt/domain/entities/task_arrive_entity.dart';
import 'package:maibu_satabot_v2/core/network/mqtt/domain/entities/task_status_entity.dart';
import '../../../../core/logging/i_logger_service.dart';
import '../../../../core/logging/log_time.dart';
import '../../../../core/network/net_message_dispatcher.dart';
import '../../../../core/network/tcp/tcp_client.dart';
import 'device_status_event.dart';
@@ -20,6 +22,10 @@ import 'devices_cubit.dart';
// 定义全局的TaskMessageRepository获取方式
TaskMessageRepository get _taskMessageRepo => GetIt.I<TaskMessageRepository>();
// 🔥 MQTT机器状态数据源(替代TCP 0x02)
MowerRealtimeRepository get _mowerRealtimeRepo =>
GetIt.I<MowerRealtimeRepository>();
class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
final NetMessageDispatcher _dispatcher;
final TcpClient tcpClient; // 🔥 新增:直接访问 TcpClient
@@ -30,6 +36,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
StreamSubscription? _mqttArriveSubscription;
StreamSubscription? _mqttStatusSubscription;
// 🔥 MQTT机器状态订阅(车辆实时+定位,替代TCP 0x02)
StreamSubscription? _mqttVehicleSubscription;
StreamSubscription? _mqttLocationSubscription;
// 🔥 节流相关:500ms节流控制0x02数据推送频率
Timer? _throttleTimer;
static const _throttleDuration = Duration(milliseconds: 500);
@@ -42,11 +52,17 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
// 🔥 当前监听的设备ID
String? _currentDeviceId;
// 🔥 MQTT机器状态收帧计数(用于链路日志)
int _mqttVehicleFrameCount = 0;
int _mqttLocationFrameCount = 0;
DeviceStatusBloc(this._dispatcher, {TcpClient? client})
: tcpClient = client ?? GetIt.I<TcpClient>(),
super(DeviceStatusInitial()) {
// 🔥 核心改动:直接在构造函数中建立TCP监听,类似RemoteControlCubit
_initDirectTcpListener();
// 🔥 已停用:机器状态数据源已改为MQTT(见 startMqttRealtimeListening,
// 由用户切换设备时触发)。TCP监听逻辑保留,回滚时取消下行注释即可。
// _initDirectTcpListener();
// 🔥 初始化MQTT到达点监听
_initMqttArriveListener();
@@ -191,6 +207,86 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
_logger.log('✅ [DeviceStatusBloc] 直接TCP监听器已建立完成');
}
/// 🔥 MQTT机器状态监听(替代TCP 0x02数据源)
///
/// [deviceSn] 与TCP时代 switchDevice / connectBySwitch 的设备号获取方式一致
/// (即 DeviceEntity.deviceName),由用户点击设备列表切换设备时调用。
///
/// 第一阶段:订阅并打印原始报文;确认格式后在第二阶段解析为
/// RunningStatusEntity + GPSEntity,复用现有500ms节流逻辑。
void startMqttRealtimeListening(String deviceSn) {
if (deviceSn.isEmpty) {
debugPrint('${LogTime.now()} ⚠️ [DeviceStatusBloc] MQTT订阅跳过:设备SN为空');
return;
}
debugPrint('${LogTime.now()} 🚀 [DeviceStatusBloc] 启动MQTT机器状态监听: $deviceSn');
// 按SN订阅(数据源内部自动退旧订新、同SN去重)
_mowerRealtimeRepo.startListening(deviceSn: deviceSn).then((result) {
result.fold(
(failure) => debugPrint(
'${LogTime.now()} ❌ [DeviceStatusBloc] MQTT机器状态订阅失败: ${failure.message}',
),
(_) => debugPrint(
'${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态订阅完成: $deviceSn',
),
);
});
// 切换设备时重置收帧计数,便于观察新设备的首帧与频率
_mqttVehicleFrameCount = 0;
_mqttLocationFrameCount = 0;
// 流监听只建立一次,第一阶段仅打印解析结果验证链路
_mqttVehicleSubscription ??= _mowerRealtimeRepo.vehicleStream.listen((
message,
) {
_mqttVehicleFrameCount++;
debugPrint(
'${LogTime.now()} 📊 [MQTT-机器状态] 车辆实时第$_mqttVehicleFrameCount帧 type=${message.type} 数据点=${message.data.length}',
);
// TODO 第二阶段:将数据点映射到 RunningStatusEntity 字段,复用500ms节流emit
});
_mqttLocationSubscription ??= _mowerRealtimeRepo.locationStream.listen((
location,
) {
_mqttLocationFrameCount++;
debugPrint(
'${LogTime.now()} 📍 [MQTT-location] 定位第$_mqttLocationFrameCount帧 lat=${location.latitude}, lng=${location.longitude}, heading=${location.heading}',
);
// 🔥 核心:用 MQTT location 数据更新缓存并节流 emit
if (location.isValid) {
// 更新 _cachedGps
_cachedGps = GPSEntity(location.latitude!, location.longitude!);
// 更新 _cachedStatus 中的航向角和坐标(复用已有缓存或新建空壳)
final base = _cachedStatus ?? RunningStatusEntity();
_cachedStatus = base.copyWith(
latitude: location.latitude!,
longitude: location.longitude!,
yaw: location.heading ?? base.yaw,
);
// 500ms节流:与 TCP 0x02 时代逻辑一致
if (_throttleTimer == null || !_throttleTimer!.isActive) {
_throttleTimer = Timer(_throttleDuration, () {
_emitCachedStatus();
});
}
} else {
debugPrint(
'${LogTime.now()} ⚠️ [MQTT-location] 坐标无效,跳过 - lat=${location.latitude}, lng=${location.longitude}',
);
}
});
debugPrint('${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
_logger.log('✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
}
// 🔥 初始化MQTT到达点监听
void _initMqttArriveListener() {
debugPrint('🔗 [DeviceStatusBloc] 初始化MQTT到达点监听器');
@@ -304,7 +400,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
// 🔥 节流发射:500ms到期后发射缓存的最新数据
void _emitCachedStatus() {
if (_cachedStatus != null && _cachedGps != null && !isClosed) {
debugPrint('📤 [0x02] 节流发射 | 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) | ${DateTime.now().toString().substring(11, 19)}');
debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) yaw=${_cachedStatus!.yaw} | ${DateTime.now().toString().substring(11, 19)}');
emit(DeviceStatusUpdated(_cachedStatus!, _cachedGps!));
}
}
@@ -423,6 +519,12 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
_mqttStatusSubscription?.cancel();
_mqttStatusSubscription = null;
// 🔥 清理MQTT机器状态订阅
_mqttVehicleSubscription?.cancel();
_mqttVehicleSubscription = null;
_mqttLocationSubscription?.cancel();
_mqttLocationSubscription = null;
// 🔥 清理节流timer和缓存
_throttleTimer?.cancel();
_throttleTimer = null;