加上tracepoint版本

This commit is contained in:
mmc
2026-02-26 14:08:02 +08:00
parent a296c7059d
commit de093bc867
3 changed files with 653 additions and 61 deletions

View File

@@ -18,6 +18,7 @@ class RunningStatusPage extends StatefulWidget {
}
class _RunningStatusPageState extends State<RunningStatusPage> {
final _dispatcher = sl<NetMessageDispatcher>();
late StreamSubscription<RawPacket> _sub;

View File

@@ -0,0 +1,343 @@
import 'dart:convert';
import 'dart:async';
// ======================== 枚举定义 ========================
/// 轨迹模式枚举
enum TPMode {
NAVIGATION, // 导航模式:绘制规划已完成路径和当前路径
LOCATION, // 定位模式:仅绘制当前点
TRACK, // 轨迹模式:实时绘制历史轨迹
}
/// 导航模式下的操作动作枚举
enum TPAction {
UPDATE, // 更新当前点
ADD, // 添加完成点
}
// ======================== 尝试锁实现 ========================
class TryLock {
bool _locked = false;
/// 尝试获取锁
/// return: true-获取成功,false-已被锁定
bool tryLock() {
if (_locked) return false;
_locked = true;
return true;
}
/// 释放锁
void release() {
_locked = false;
}
/// 检查是否已锁定
bool get isLocked => _locked;
}
// ======================== 环形队列实现 ========================
class CircQueue<T> {
final int capacity; // 队列容量
final List<T?> buffer; // 存储缓冲区
int head = 0; // 队头索引
int tail = 0; // 队尾索引
bool isFull = false; // 队列满标志
final bool deepCopy; // 是否深拷贝元素
final int maxMemoryBytes; // 最大内存限制(字节)
int currentMemoryBytes = 0; // 当前已使用内存
CircQueue(
this.capacity, {
this.deepCopy = true,
this.maxMemoryBytes = 1024 * 1024, // 默认1MB
}) : buffer = List<T?>.filled(capacity, null);
/// 清空队列
void clear() {
head = tail;
isFull = false;
currentMemoryBytes = 0;
}
/// 判断队列是否为空
bool isEmpty() {
return head == tail && !isFull;
}
/// 判断队列是否已满
bool isFullFn() {
return isFull;
}
/// 估算对象大小(JSON序列化后的字节长度)
int _estimateSize(T item) {
try {
return utf8.encode(jsonEncode(item)).length;
} catch (e) {
return 0;
}
}
/// 深拷贝对象
T? _deepClone(T? item) {
if (!deepCopy || item == null) return item;
try {
return jsonDecode(jsonEncode(item)) as T;
} catch (e) {
return item;
}
}
/// 入队操作
bool enter(T item) {
final itemSize = _estimateSize(item);
// 检查队列满或内存超限
if (isFullFn() || (currentMemoryBytes + itemSize > maxMemoryBytes)) {
return false;
}
final clone = _deepClone(item);
buffer[tail] = clone;
tail = (tail + 1) % capacity;
currentMemoryBytes += itemSize;
if (tail == head) isFull = true;
return true;
}
/// 出队操作
T? out() {
if (isEmpty()) return null;
final item = buffer[head];
if (item != null) {
currentMemoryBytes -= _estimateSize(item);
}
head = (head + 1) % capacity;
isFull = false;
return _deepClone(item);
}
/// 丢弃指定数量的元素
bool discard(int len) {
for (int i = 0; i < len; i++) {
if (isEmpty()) return false;
final index = (tail - 1 + capacity) % capacity;
final item = buffer[index];
if (item != null) {
currentMemoryBytes -= _estimateSize(item);
}
tail = index;
isFull = false;
}
return true;
}
/// 获取队列元素个数
int getDepth() {
return isFull ? capacity : (tail + capacity - head) % capacity;
}
/// 异步入队(支持超时)
Future<bool> send(T item, {int timeoutMs = 0}) async {
final start = DateTime.now().millisecondsSinceEpoch;
while (!enter(item)) {
if (timeoutMs > 0 &&
DateTime.now().millisecondsSinceEpoch - start >= timeoutMs) {
return false;
}
await Future.delayed(const Duration(milliseconds: 1));
}
return true;
}
/// 异步出队(支持超时)
Future<T?> recv({int timeoutMs = 0}) async {
final start = DateTime.now().millisecondsSinceEpoch;
while (true) {
final item = out();
if (item != null) return item;
if (timeoutMs > 0 &&
DateTime.now().millisecondsSinceEpoch - start >= timeoutMs) {
return null;
}
await Future.delayed(const Duration(milliseconds: 1));
}
}
/// 转换为列表(用于调试/序列化)
List<T?> toList() {
final result = <T?>[];
int i = head;
int count = getDepth();
while (count-- > 0) {
result.add(_deepClone(buffer[i]));
i = (i + 1) % capacity;
}
return result;
}
}
class TracePoint<T> {
// 私有成员变量
late final CircQueue<T> _queue;
List<T> _tracePoint = [];
int _completePointIndex = 0;
late final TryLock _lock;
TPMode _mode = TPMode.LOCATION;
// 公开的回调(无下划线,外部可访问)
void Function(T)? onCurrentPointUpdated;
void Function(T)? onCompletePointAdded;
// 测试用方法
bool tryLock() => _lock.tryLock();
void release() => _lock.release();
/// 构造函数
TracePoint({int queueCapacity = 5, int maxMemoryBytes = 1024 * 1024}) {
_queue = CircQueue<T>(
queueCapacity,
deepCopy: true,
maxMemoryBytes: maxMemoryBytes,
);
_lock = TryLock();
reset();
}
/// 复位所有状态
void reset() {
_queue.clear();
_tracePoint = [];
_completePointIndex = 0;
_lock.release();
}
/// 设置工作模式
void setMode(TPMode mode) {
reset();
_mode = mode;
}
/// 添加/更新轨迹点
void upsert(T point, [TPAction act = TPAction.UPDATE]) {
switch (_mode) {
case TPMode.NAVIGATION:
act == TPAction.UPDATE
? _updateCurrentPoint(point)
: _addCompletePoint(point);
break;
case TPMode.LOCATION:
_updateCurrentPoint(point);
break;
case TPMode.TRACK:
_addCompletePoint(point);
break;
}
}
/// 获取轨迹点列表(深拷贝,线程安全)
List<T>? getTracePoint() {
if (_lock.tryLock()) {
final trace = List<T>.from(_tracePoint);
_lock.release();
return trace;
} else {
print('getTracePoint: lock!!!');
return null;
}
}
/// 更新当前点(私有方法)
void _updateCurrentPoint(T point) {
if (point == null) {
print('updateCurrentPoint: point is NULL');
return;
}
if (_lock.tryLock()) {
try {
switch (_mode) {
case TPMode.NAVIGATION:
if (_tracePoint.isEmpty) {
print(
'updateCurrentPoint: tracePoint is empty , wait first completed point, discard current point: $point',
);
} else {
_tracePoint = _tracePoint.sublist(0, _completePointIndex);
_tracePoint.add(point);
// 触发公开的回调(无下划线)
onCurrentPointUpdated?.call(point);
}
break;
case TPMode.LOCATION:
if (_tracePoint.isEmpty) {
_tracePoint.add(point);
} else {
_tracePoint[0] = point;
}
// 触发公开的回调(无下划线)
onCurrentPointUpdated?.call(point);
break;
default:
break;
}
} finally {
_lock.release();
}
} else {
print('updateCurrentPoint: lock!!!, discard current point: $point');
}
}
/// 添加完成点(私有方法)
void _addCompletePoint(T point) {
if (point == null) {
print('addCompletePoint: point is NULL');
return;
}
if (_lock.tryLock()) {
try {
while (!_queue.isEmpty()) {
final queuedPoint = _queue.out();
if (queuedPoint != null) {
_addPoint(queuedPoint);
}
}
_addPoint(point);
// 触发公开的回调(无下划线)
onCompletePointAdded?.call(point);
} catch (e) {
print('addCompletePoint failure: $e');
} finally {
_lock.release();
}
} else {
_queue.enter(point);
print('addCompletePoint: lock!!!, enter queue, wait disposing: $point');
}
}
/// 内部添加点逻辑(私有方法)
void _addPoint(T point) {
if (point == null) {
print('point NULL:');
return;
}
final hasCurrentPoint =
_tracePoint.isNotEmpty && _completePointIndex < _tracePoint.length;
if (hasCurrentPoint) {
_tracePoint = _tracePoint.sublist(0, _completePointIndex);
_tracePoint.insert(_completePointIndex, point);
} else {
_tracePoint.add(point);
}
_completePointIndex++;
}
}

View File

@@ -1,17 +1,25 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
import '../path_list_pages.dart';
// 定义轨迹点类型(经纬度)
typedef PlotPoint = LatLng;
class PlotData {
final String id; // 唯一ID,用于删除
final String plotName; // 地块名称
@@ -28,8 +36,13 @@ class MapPageEnterprise extends StatefulWidget {
}
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final _dispatcher = sl<NetMessageDispatcher>();
late StreamSubscription<RawPacket> _sub;
final MapController _mapController = MapController();
final TextEditingController _plotNameController = TextEditingController();
// 初始化轨迹管理器(泛型指定为LatLng)
late final TracePoint<PlotPoint> _traceManager;
LatLng? _currentLatLng;
StreamSubscription<Position>? _positionSub;
@@ -38,6 +51,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
bool _directionBoxOpen = false; //航线面板显示/隐藏
final bool _saveBoxOpen = true; //保存按钮显示/隐藏
bool _isListBoxOpen = false; //列表面板显示/隐藏
// 新增:存储选中的地块(null表示未选中)
PlotData? _selectedPlot;
// 新增:控制底部作业面板显示
bool _isWorkPanelOpen = false;
RobotMode _currentRobotMode = RobotMode.point; //打点模式
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
@@ -47,6 +64,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
final double _headingAngle = 0.0; // 当前机器航向角(单位:度)
List<LatLng>? _currentTracePoints; //收到机器的点坐标
//打点
double _workDistance = 0.0; // 作业行距(单位:米)
@@ -62,17 +80,34 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
PlotData(id: '2', plotName: '东地块(二号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '4', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '5', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '6', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '7', plotName: '西地块(三号田)', imageUrl: ''),
PlotData(id: '8', plotName: '西地块(三号田)', imageUrl: ''),
];
@override
void initState() {
super.initState();
_traceManager = TracePoint<PlotPoint>();
_initLocationEnterprise();
_sub = _dispatcher.onCommand(0x12).listen(_onDeviceData);
// 设置事件回调(更新UI)
_traceManager.onCurrentPointUpdated = (point) {
setState(() {
_currentTracePoints = _traceManager.getTracePoint();
});
};
_traceManager.onCompletePointAdded = (point) {
setState(() {
_currentTracePoints = _traceManager.getTracePoint();
});
};
// 示例:切换到导航模式
_traceManager.setMode(TPMode.LOCATION);
// 监听地图移动事件,实时更新连线
_mapController.mapEventStream.listen((event) {
if (event is MapEventMove || event is MapEventMoveEnd) {
@@ -98,6 +133,38 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
});
}
void _onDeviceData(RawPacket packet) {
try {
debugPrint('>>> 收到0x12设备数据: ${packet}');
debugPrint('>>> 收到设备数据: ${utf8.decode(packet.payload)}');
final csv = utf8.decode(packet.payload);
final fields = csv.split(',');
// 至少需 16 个字段(索引 10=航向角, 13=卫星数, 14=定位质量)
if (fields.length >= 16) {
//setState(() {
// _voltage = fields[0]; // 电压
// _leftSpeed = fields[1]; // 左轮目标速度
// _rightSpeed = fields[2]; // 右轮目标速度
// _pitch = fields[11]; // 俯仰角
// _roll = fields[12]; // 翻滚角
// _latitude = fields[16]; // 纬度
// _longitude = fields[17]; // 经度
// _timeStamp = '${fields[16]} ${fields[17]}'; // 时间戳(合并字段)
// _knifeCuttingSpeed = fields[19]; // 割刀速度
// _controlMode = fields[20]; // 控制模式
// _workingArea = fields[21]; // 作业面积
// _battery = fields[22]; // 电量
// _obstacleFlag = fields[23]; // 障碍物标志
// _yaw = fields[10]; // 航向角
// _satelliteCnt = fields[13]; // 卫星数
// _qual = fields[14]; // 定位质量
//});
}
} catch (_) {
// 解析失败时忽略,避免崩溃
}
}
void _showSavePlotDialog() {
// 清空上次的输入内容
_plotNameController.clear();
@@ -290,69 +357,114 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
@override
void dispose() {
_positionSub?.cancel();
_traceManager.reset();
_mapController.dispose();
_sub?.cancel();
super.dispose();
}
// 模拟添加完成点
void _addCompletePoint() {
final newPoint = LatLng(
39.9042 + (DateTime.now().millisecond / 10000),
116.4074 + (DateTime.now().millisecond / 10000),
);
_traceManager.upsert(newPoint, TPAction.ADD);
}
// 模拟更新当前点
void _updateCurrentPoint() {
final newPoint = LatLng(
39.9042 + (DateTime.now().millisecond / 10000),
116.4074 + (DateTime.now().millisecond / 10000),
);
_traceManager.upsert(newPoint);
}
// 在State类中添加列表项构建方法
Widget _buildPlotListItem(PlotData plot) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[100]!),
),
child: Row(
children: [
// 1. 地块图片
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
plot.imageUrl,
// 网络图片替换为:Image.network(plot.imageUrl, ...)
width: 40,
height: 40,
fit: BoxFit.cover,
// 图片加载失败占位
errorBuilder: (context, error, stackTrace) {
return Container(
width: 40,
height: 40,
color: Colors.grey[200],
child: const Icon(Icons.image_outlined, color: Colors.grey),
);
return GestureDetector(
// 核心:点击列表项触发选中逻辑
onTap: () {
setState(() {
// 1. 选中当前地块
_selectedPlot = plot;
// 2. 关闭列表抽屉
_isListBoxOpen = false;
// 3. 打开底部作业面板
_isWorkPanelOpen = true;
});
//// 可选:提示选中成功
//ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('已选中地块:${plot.plotName}'),
// duration: const Duration(seconds: 1),
// ),
//);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _selectedPlot?.id == plot.id
? Colors.blue
: Colors.grey[100]!,
width: _selectedPlot?.id == plot.id ? 2 : 1, // 选中项高亮边框
),
),
child: Row(
children: [
// 1. 地块图片(原有代码)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
plot.imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 60,
height: 60,
color: Colors.grey[200],
child: const Icon(Icons.image_outlined, color: Colors.grey),
);
},
),
),
const SizedBox(width: 12),
// 2. 地块名称(原有代码)
Expanded(
child: Text(
plot.plotName,
style: const TextStyle(fontSize: 16, color: Colors.black87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// 3. 删除按钮(原有代码)
IconButton(
onPressed: () {
_showDeleteConfirmDialog(plot);
},
icon: const Icon(
Icons.delete_outline,
color: Colors.redAccent,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
const SizedBox(width: 12),
// 2. 地块名称(占满剩余空间)
Expanded(
child: Text(
plot.plotName,
style: const TextStyle(fontSize: 16, color: Colors.black87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// 3. 删除按钮
IconButton(
onPressed: () {
// 弹出确认删除对话框
_showDeleteConfirmDialog(plot);
},
icon: const Icon(
Icons.delete_outline,
color: Colors.redAccent,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
],
),
),
);
}
@@ -398,6 +510,139 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}
// 注意:方法名改为 _buildWorkPanel(因为实际是作业面板,不是列表,避免混淆)
Widget _buildWorkPanel() {
// 核心修复1:空安全判断 - 无选中地块时返回空容器
if (_selectedPlot == null) {
return const SizedBox.shrink();
}
return Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min, // 关键:只占用内容高度,不挤压其他组件
children: [
// 1. 选中地块信息行
Row(
crossAxisAlignment: CrossAxisAlignment.center, // 修复:垂直居中对齐
children: [
// 地块图片
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
_selectedPlot!.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
// 核心修复2:图片加载错误占位优化
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Icon(
Icons.image_outlined,
color: Colors.grey,
size: 32,
),
);
},
),
),
const SizedBox(width: 16),
// 地块名称(修复:占满剩余空间)
Expanded(
child: Text(
_selectedPlot!.plotName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
// 关闭按钮(修复:减少点击区域,避免误触)
Padding(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
onPressed: () {
setState(() {
_isWorkPanelOpen = false;
_selectedPlot = null; // 清空选中状态
});
},
icon: const Icon(Icons.close, color: Colors.grey, size: 20),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: () {
// 核心修复3:再次空安全校验(双重保险)
if (_selectedPlot != null) {
//_startWork(_selectedPlot!);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00C853), // 绿色主题
foregroundColor: Colors.white, // 文字颜色(Flutter 3.0+ 推荐)
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
shadowColor: Colors.black12,
padding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 16,
),
minimumSize: const Size(double.infinity, 48),
),
child: const Text(
'开始作业',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.0,
),
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
@@ -607,6 +852,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
),
),
),
//开始作业面板
// 底部操作面板
if (_isPanelOpen)
@@ -806,6 +1052,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
],
),
),
if (_isWorkPanelOpen) _buildWorkPanel(),
],
),
),