完成页面对接tcp推送的设备状态数据

完成页面测试解析tcp推送的设备状态数据的UI展示
This commit is contained in:
2026-03-03 19:34:17 +08:00
parent 0a3a062c04
commit 34b4c2bcbc
12 changed files with 676 additions and 230 deletions

View File

@@ -1,14 +1,25 @@
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import '../../../../../core/storage/user_storage.dart';
import '../../../domain/entities/device_location_entity.dart';
class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
final Dio dio;
final UserStorage _userStorage; // 🔥 新增字段
DeviceHttpDatasourceImpl(this.dio);
// 🔥 修改构造函数,注入 UserStorage
DeviceHttpDatasourceImpl(this.dio, this._userStorage);
// 🔥 辅助方法:获取 Token
Future<String?> _getToken() async {
final user = await _userStorage.getUser();
debugPrint('用户信息: $user') ;
return user?.token;
}
@override
Future<int> bindDevice(String deviceId, String deviceAlias) async {
@@ -31,9 +42,17 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<List<DeviceEntity>> getUserDevices(String username) async {
final token = await _getToken();
var response = await dio.get(
HttpApiConsts.getUserDevicesList,
queryParameters: {'tenantName': username},
options: Options(
headers: {
'Authorization': token != null ? 'Bearer $token' : '',
// 如果后端不需要 Bearer 前缀,直接写 token 即可
},
),
);
if (response.statusCode != 200) {
throw Exception('网络请求失败:${response.statusCode}');
@@ -58,9 +77,16 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> switchDevice(String platform, String deviceId) async {
final token = await _getToken();
var response = await dio.post(
HttpApiConsts.switchDevice,
data: {'platform': platform, 'deviceId': deviceId},
options: Options(
headers: {
'Authorization': token != null ? 'Bearer $token' : '',
// 如果后端不需要 Bearer 前缀,直接写 token 即可
},
),
);
if (response.statusCode != 200) {
throw Exception('网络请求失败:${response.statusCode}');

View File

@@ -1,29 +1,43 @@
// lib/features/devices/domain/entities/running_status_entity.dart
import 'package:equatable/equatable.dart';
class RunningStatusEntity extends Equatable {
final double voltage;
final double leftTargetSpeed;
final double rightTargetSpeed;
final double leftMeasureSpeed;
final double rightMeasureSpeed;
final double leftCurrent;
final double rightCurrent;
final double leftMotorTemp;
final double rightMotorTemp;
final double chipTemp;
final double yaw;
final double pitch;
final double roll;
final int satelliteCnt;
final int qual;
final int headingStatus;
final double latitude;
final double longitude;
final String knifeCuttingSpeed;
final String controlMode;
final String battery;
final String workingArea;
final String obstacleFlag;
// === 基础电气信息 (字段 0-6) ===
final double voltage; // [0] 电压 (V)
final double leftTargetSpeed; // [1] 左轮目标速度
final double rightTargetSpeed; // [2] 右轮目标速度
final double leftMeasureSpeed; // [3] 左轮实测速度
final double rightMeasureSpeed; // [4] 右轮实测速度
final double leftCurrent; // [5] 左轮电流 (A)
final double rightCurrent; // [6] 右轮电流 (A)
// === 温度信息 (字段 7-9) ===
final double leftMotorTemp; // [7] 左电机温度 (℃)
final double rightMotorTemp; // [8] 右电机温度 (℃)
final double chipTemp; // [9] 芯片温度 (℃)
// === 姿态信息 (字段 10-12) ===
final double yaw; // [10] 偏航角 (°)
final double pitch; // [11] 俯仰角 (°)
final double roll; // [12] 翻滚角 (°)
// === GPS 信号质量 (字段 13-15) ===
final int satelliteCnt; // [13] 卫星数量
final int qual; // [14] 定位质量
final int headingStatus; // [15] 航向状态
// === GPS 位置信息 (字段 16-17) ===
final double latitude; // [16] 纬度
final double longitude; // [17] 经度
// === 扩展信息 (字段 18-23) ===
final String timestamp; // [18] 时间戳 (格式:20-1-11-31 08:00:00)
final String knifeCuttingSpeed; // [19] 割刀速度
final String controlMode; // [20] 控制模式
final String battery; // [21] 电量百分比
final String workingArea; // [22] 工作面积
final String obstacleFlag; // [23] 障碍物标志
const RunningStatusEntity({
this.voltage = 0.0,
@@ -44,13 +58,61 @@ class RunningStatusEntity extends Equatable {
this.headingStatus = 0,
this.latitude = 0.0,
this.longitude = 0.0,
this.knifeCuttingSpeed = "0",
this.controlMode = "0",
this.battery = "0",
this.workingArea = "0",
this.obstacleFlag = "0",
this.timestamp = '',
this.knifeCuttingSpeed = '0',
this.controlMode = '0',
this.battery = '0',
this.workingArea = '0',
this.obstacleFlag = '0',
});
/// 从字段数组创建实体 (核心解析方法)
factory RunningStatusEntity.fromFields(List<String> fields) {
// 确保至少有 24 个字段,不足的用默认值填充
while (fields.length < 24) {
fields.add('');
}
return RunningStatusEntity(
// 基础电气信息
voltage: double.tryParse(fields[0]) ?? 0.0,
leftTargetSpeed: double.tryParse(fields[1]) ?? 0.0,
rightTargetSpeed: double.tryParse(fields[2]) ?? 0.0,
leftMeasureSpeed: double.tryParse(fields[3]) ?? 0.0,
rightMeasureSpeed: double.tryParse(fields[4]) ?? 0.0,
leftCurrent: double.tryParse(fields[5]) ?? 0.0,
rightCurrent: double.tryParse(fields[6]) ?? 0.0,
// 温度信息
leftMotorTemp: double.tryParse(fields[7]) ?? 0.0,
rightMotorTemp: double.tryParse(fields[8]) ?? 0.0,
chipTemp: double.tryParse(fields[9]) ?? 0.0,
// 姿态信息
yaw: double.tryParse(fields[10]) ?? 0.0,
pitch: double.tryParse(fields[11]) ?? 0.0,
roll: double.tryParse(fields[12]) ?? 0.0,
// GPS 信号质量
satelliteCnt: int.tryParse(fields[13]) ?? 0,
qual: int.tryParse(fields[14]) ?? 0,
headingStatus: int.tryParse(fields[15]) ?? 0,
// GPS 位置信息
latitude: double.tryParse(fields[16]) ?? 0.0,
longitude: double.tryParse(fields[17]) ?? 0.0,
// 扩展信息 (保持原始字符串,由调用方决定如何解析)
timestamp: fields[18].trim(),
knifeCuttingSpeed: fields[19].trim().isNotEmpty ? fields[19].trim() : '0',
controlMode: fields[20].trim().isNotEmpty ? fields[20].trim() : '0',
battery: fields[21].trim().isNotEmpty ? fields[21].trim() : '0',
workingArea: fields[22].trim().isNotEmpty ? fields[22].trim() : '0',
obstacleFlag: fields[23].trim().isNotEmpty ? fields[23].trim() : '0',
);
}
/// copyWith 方法 (用于局部更新)
RunningStatusEntity copyWith({
double? voltage,
double? leftTargetSpeed,
@@ -70,6 +132,7 @@ class RunningStatusEntity extends Equatable {
int? headingStatus,
double? latitude,
double? longitude,
String? timestamp,
String? knifeCuttingSpeed,
String? controlMode,
String? battery,
@@ -95,6 +158,7 @@ class RunningStatusEntity extends Equatable {
headingStatus: headingStatus ?? this.headingStatus,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
timestamp: timestamp ?? this.timestamp,
knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed,
controlMode: controlMode ?? this.controlMode,
battery: battery ?? this.battery,
@@ -105,9 +169,35 @@ class RunningStatusEntity extends Equatable {
@override
List<Object?> get props => [
voltage, leftTargetSpeed, rightTargetSpeed, leftMeasureSpeed, rightMeasureSpeed,
leftCurrent, rightCurrent, leftMotorTemp, rightMotorTemp, chipTemp,
yaw, pitch, roll, satelliteCnt, qual, headingStatus,
latitude, longitude, knifeCuttingSpeed, controlMode, battery, workingArea, obstacleFlag,
voltage,
leftTargetSpeed,
rightTargetSpeed,
leftMeasureSpeed,
rightMeasureSpeed,
leftCurrent,
rightCurrent,
leftMotorTemp,
rightMotorTemp,
chipTemp,
yaw,
pitch,
roll,
satelliteCnt,
qual,
headingStatus,
latitude,
longitude,
timestamp,
knifeCuttingSpeed,
controlMode,
battery,
workingArea,
obstacleFlag,
];
@override
String toString() {
return 'RunningStatusEntity(voltage: $voltage, chipTemp: $chipTemp, '
'latitude: $latitude, longitude: $longitude, timestamp: $timestamp)';
}
}

View File

@@ -14,77 +14,82 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
final NetMessageDispatcher _dispatcher;
DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) {
// 直接订阅 0x02 指令的字符串流
// 订阅 TCP 数据流
_dispatcher.onStringMessage().listen((jsonString) {
debugPrint('📩 Bloc 收到 0x02 数据');
add(DeviceStatusLoaded(jsonString));
});
// 订阅 0x12 的 JSON 流()
_dispatcher.onJsonMessage(0x12).listen((jsonData) {
add(PushMessageReceived(jsonData));
});
}
@override
Stream<DeviceStatusState> mapEventToState(DeviceStatusEvent event) async* {
if (event is DeviceStatusLoaded) {
yield* _mapDeviceStatusLoaded(event);
} else if (event is PushMessageReceived) {
yield* _mapPushMessageReceived(event);
}
}
//
on<DeviceStatusLoaded>(_handleDeviceStatusLoaded);
on<PushMessageReceived>(_handlePushMessageReceived);
Stream<DeviceStatusState> _mapDeviceStatusLoaded(DeviceStatusLoaded event) async* {
// 🔥 新增:模拟测试数据(调试用)
_testMockData();
}
void _testMockData() {
// 用户提供的真实指令数据(字节数组)
final mockBytes = [
171, 170, 2, 48, 46, 48, 48, 44, 48, 44, 48, 44, 48, 44, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 50, 55, 46, 52, 55, 44, 49, 56, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 52, 49, 44, 52, 44, 49, 44, 51, 50, 46, 48, 52, 49, 50, 57, 54, 56, 54, 52, 48, 48, 57, 57, 57, 44, 49, 50, 48, 46, 56, 48, 56, 57, 57, 51, 48, 56, 49, 49, 53, 56, 53, 48, 44, 50, 48, 45, 49, 45, 49, 49, 45, 51, 49, 32, 48, 56, 58, 48, 48, 58, 48, 48, 44, 48, 44, 51, 44, 54, 54, 44, 57, 53, 50, 55, 46, 48, 48, 48, 48, 44, 48, 0, 0, 0, 170, 171
];
// 解码为字符串(去掉帧头 3 字节和帧尾 5 字节)
final payloadBytes = mockBytes.sublist(3, mockBytes.length - 5);
final mockData = String.fromCharCodes(payloadBytes);
debugPrint('🧪 模拟测试数据:$mockData');
// 发送模拟事件
add(DeviceStatusLoaded(mockData));
}
// 🔥 修改:使用 Emitter 发送状态
Future<void> _handleDeviceStatusLoaded(
DeviceStatusLoaded event,
Emitter<DeviceStatusState> emit,
) async {
try {
final fields = event.jsonString.trim().split(' ');
debugPrint('🔍 开始解析数据:${event.jsonString}');
final fields = event.jsonString.trim().split(',');
debugPrint('🔍 字段数量:${fields.length}');
if (fields.length < 18) {
yield DeviceStatusError('字段不足,期望 ≥18,实际: ${fields.length}');
emit(DeviceStatusError('字段不足,期望 ≥18,实际:${fields.length}'));
return;
}
///解析设备状态
final status = RunningStatusEntity().copyWith(
voltage: double.tryParse(fields[0]) ?? 0.0,
leftTargetSpeed: double.tryParse(fields[1]) ?? 0.0,
rightTargetSpeed: double.tryParse(fields[2]) ?? 0.0,
leftMeasureSpeed: double.tryParse(fields[3]) ?? 0.0,
rightMeasureSpeed: double.tryParse(fields[4]) ?? 0.0,
leftCurrent: double.tryParse(fields[5]) ?? 0.0,
rightCurrent: double.tryParse(fields[6]) ?? 0.0,
leftMotorTemp: double.tryParse(fields[7]) ?? 0.0,
rightMotorTemp: double.tryParse(fields[8]) ?? 0.0,
chipTemp: double.tryParse(fields[9]) ?? 0.0,
yaw: double.tryParse(fields[10]) ?? 0.0,
pitch: double.tryParse(fields[11]) ?? 0.0,
roll: double.tryParse(fields[12]) ?? 0.0,
satelliteCnt: int.tryParse(fields[13]) ?? 0,
qual: int.tryParse(fields[14]) ?? 0,
headingStatus: int.tryParse(fields[15]) ?? 0,
latitude: double.tryParse(fields[16]) ?? 0.0,
longitude: double.tryParse(fields[17]) ?? 0.0,
knifeCuttingSpeed: fields.length > 19 && fields[19].isNotEmpty ? fields[19] : "0",
controlMode: fields.length > 20 && fields[20].isNotEmpty ? fields[20] : "0",
battery: fields.length > 21 && fields[21].isNotEmpty ? fields[21] : "0",
workingArea: fields.length > 22 && fields[22].isNotEmpty ? fields[22] : "0",
obstacleFlag: fields.length > 23 && fields[23].isNotEmpty ? fields[23] : "0",
);
///解析GPS
final status = RunningStatusEntity.fromFields(fields);
debugPrint('设备运行时候状态:$status');
final gps = GPSEntity(status.latitude, status.longitude);
yield DeviceStatusUpdated(status, gps);
debugPrint('GPS: ${gps.latitude}, ${gps.longitude}');
emit(DeviceStatusUpdated(status, gps)); // 🔥 使用 emit 发送状态
} catch (e) {
yield DeviceStatusError('解析设备状态失败: $e');
debugPrint('❌ 解析异常:$e');
emit(DeviceStatusError('解析设备运行时候状态失败:$e'));
}
}
/// 解析 推送消息
Stream<DeviceStatusState> _mapPushMessageReceived(PushMessageReceived event) async* {
Future<void> _handlePushMessageReceived(
PushMessageReceived event,
Emitter<DeviceStatusState> emit,
) async {
try {
final eventStr = event.jsonData['event'] ?? '';
final deviceId = event.jsonData['deviceId'] ?? '未知';
debugPrint('收到推送事件: $eventStr, 设备: $deviceId');
// 可扩展:yield PushMessageState(event.jsonData)
debugPrint('收到推送事件:$eventStr, 设备:$deviceId');
} catch (e) {
yield DeviceStatusError('解析推送消息失败: $e');
emit(DeviceStatusError('解析推送消息失败:$e'));
}
}
@override
Future<void> close() {
return super.close();
}
}

View File

@@ -21,3 +21,4 @@ class PushMessageReceived extends DeviceStatusEvent {
@override
List<Object?> get props => [jsonData];
}