完成路径规划发送的握手机制-一对一校验应答-待测试
完成路径规划握手机制的开关功能。
This commit is contained in:
20
lib/core/env/path_planning_config.dart
vendored
Normal file
20
lib/core/env/path_planning_config.dart
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/// 路径规划配置类
|
||||
/// 用于控制路径规划功能的各种开关和配置
|
||||
class PathPlanningConfig {
|
||||
/// 🔥 全局开关:控制使用哪套路径规划逻辑
|
||||
/// - `false`: 使用现有逻辑(不等待 ACK 确认)
|
||||
/// - `true`: 使用新的 ACK 握手机制(等待 0x02 和 0x01 确认)
|
||||
static bool useAckHandshake = false;
|
||||
|
||||
/// 切换路径规划模式
|
||||
/// [useAck] true = 使用 ACK 握手机制,false = 使用现有逻辑
|
||||
static void setMode(bool useAck) {
|
||||
useAckHandshake = useAck;
|
||||
print('🔧 [PathPlanningConfig] 路径规划模式已切换:${useAck ? "ACK 握手机制" : "现有逻辑"}');
|
||||
}
|
||||
|
||||
/// 获取当前模式描述
|
||||
static String getModeDescription() {
|
||||
return useAckHandshake ? "ACK 握手机制 (等待 0x02 + 0x01)" : "现有逻辑 (仅等待 0x01)";
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart';
|
||||
|
||||
import '../../features/devices/domain/repositories/route_planning_repository.dart';
|
||||
import '../../features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../features/devices/services/path_planning_service.dart';
|
||||
import '../di/injection.dart';
|
||||
import '../env/path_planning_config.dart';
|
||||
import 'protocol_decoder.dart';
|
||||
import 'tcp/tcp_client.dart';
|
||||
// import '../../features/auth/data/models/user_model.dart';
|
||||
@@ -15,9 +17,23 @@ class NetMessageDispatcher {
|
||||
final TcpClient tcpClient;
|
||||
final PathPlanningService _pathPlanningService;
|
||||
final RoutePlanningRepository routePlanningRepository;
|
||||
// 🔥 修改:使用回调函数获取 AppState
|
||||
// 修改:使用回调函数获取 AppState
|
||||
final Function? getAppState;
|
||||
|
||||
// 🔥 新增:ACK 握手状态管理 (仅新模式使用)
|
||||
int _expectedPointIndex = -1; // 期望收到的点编号
|
||||
bool _waitingForAck = false; // 是否正在等待 ACK 确认
|
||||
bool _waitingForArrived = false; // 是否正在等待到达确认
|
||||
int _currentPointIndex = -1; // 当前发送的点索引
|
||||
|
||||
|
||||
// 🔥 新增:设置期望收到的点编号(供 PathPlanner 调用)
|
||||
void setExpectedPointIndex(int index) {
|
||||
_expectedPointIndex = index;
|
||||
debugPrint('🎯 [Dispatcher-ACK] 设置期望点编号:$index');
|
||||
}
|
||||
|
||||
|
||||
NetMessageDispatcher(this.tcpClient, this.routePlanningRepository, this._pathPlanningService, {this.getAppState});
|
||||
|
||||
/// 过滤特定指令的流
|
||||
@@ -79,7 +95,7 @@ class NetMessageDispatcher {
|
||||
/// 协议格式:AB AA 01 [DATA] CRC(2) AA AB
|
||||
/// 验证头部:data[0]=0xAB, data[1]=0xAA, data[2]=0x01
|
||||
/// 检查状态位:data[5] == 0x01 表示回复成功
|
||||
Stream<RawPacket> onPathPlanningResponse() {
|
||||
/* Stream<RawPacket> onPathPlanningResponse() {
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
|
||||
debugPrint('[Dispatcher] 开始监听路径规划指令应答 (CMD: 0x01)');
|
||||
@@ -168,13 +184,118 @@ class NetMessageDispatcher {
|
||||
|
||||
return false;
|
||||
});
|
||||
}*/
|
||||
///新版的ack和现有的融合
|
||||
/// 监听路径规划指令应答 (现有逻辑 - 不修改)
|
||||
/// 协议格式:AB AA 01 [DATA] CRC(2) AA AB
|
||||
/// 验证头部:data[0]=0xAB, data[1]=0xAA, data[2]=0x01
|
||||
/// 检查状态位:data[5] == 0x01 表示回复成功
|
||||
Stream<RawPacket> onPathPlanningResponse() {
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
|
||||
debugPrint('[Dispatcher] 开始监听路径规划指令应答 (CMD: 0x01) - 模式:${PathPlanningConfig.getModeDescription()}');
|
||||
|
||||
// 🔥 根据配置决定使用哪个流
|
||||
if (PathPlanningConfig.useAckHandshake) {
|
||||
debugPrint('⚙️ [Dispatcher] 使用 ACK 握手机制');
|
||||
return onPathPlanningResponseWithAck();
|
||||
} else {
|
||||
debugPrint('⚙️ [Dispatcher] 使用现有逻辑');
|
||||
return onCommand(0x01).where((packet) {
|
||||
// 🔥 修改:使用回调检查 AppState
|
||||
if (getAppState != null) {
|
||||
final currentState = getAppState!();
|
||||
if (currentState.toString() == 'AppState.none') {
|
||||
debugPrint('⚠️ [Dispatcher] 当前 AppState 为 none,停止处理路径规划指令');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 构造完整数据包用于验证
|
||||
final fullData = <int>[
|
||||
0xAB, 0xAA, 0x01,
|
||||
...packet.payload,
|
||||
0x00, 0x00, // CRC 占位
|
||||
0xAA, 0xAB
|
||||
];
|
||||
|
||||
// 验证头部协议
|
||||
if (fullData[0] == 0xAB &&
|
||||
fullData[1] == 0xAA &&
|
||||
fullData[2] == 0x01) {
|
||||
|
||||
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
|
||||
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
|
||||
if (fullData.length > 5 && fullData[5] == 0x01) {
|
||||
debugPrint('✅ [Dispatcher] 状态位验证通过:0x01 - 可以发送下一个指令');
|
||||
///取全局的路径规划发送实体 queue
|
||||
/// 发送下一个指令 移除上一条数据
|
||||
var queue = _pathPlanningService.getQueue();
|
||||
// 🔥 关键:必须先检查空队列!
|
||||
if (queue.isEmpty) {
|
||||
devicesCubit.finishWork();
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
devicesCubit.resetWorkStatus();
|
||||
});
|
||||
// 手动停止
|
||||
//routePlanningRepository.stopRoutePlanning();
|
||||
debugPrint('⚠️ [Dispatcher] 队列已经为空,跳过本次响应处理');
|
||||
return false;
|
||||
}
|
||||
///更新完成 jwd
|
||||
var deviceAddPathPointModel = queue.first;
|
||||
var d = deviceAddPathPointModel.latitude;
|
||||
var e = deviceAddPathPointModel.longitude;
|
||||
devicesCubit.setArrivedLocation(
|
||||
deviceAddPathPointModel.latitude,
|
||||
deviceAddPathPointModel.longitude
|
||||
);
|
||||
debugPrint("准确更新完成的路径点经纬度,$d,$e");
|
||||
//去除上一条数据
|
||||
queue.removeFirst();
|
||||
/// _pathPlanningService.updateQueue(queue as List<DeviceAddPathPointModel>);
|
||||
_pathPlanningService.updateQueue(queue.toList());
|
||||
DateTime now = DateTime.now();
|
||||
String timeStr = "${now.hour.toString().padLeft(2, '0')}:"
|
||||
"${now.minute.toString().padLeft(2, '0')}:"
|
||||
"${now.second.toString().padLeft(2, '0')}";
|
||||
|
||||
debugPrint(' $timeStr [Dispatcher] 准备触发下一个路径点发送,队列剩余:${queue.length}');
|
||||
///如果还剩 0 个数据 就停止发送
|
||||
if (queue.length==0) {
|
||||
debugPrint('⚠️ [Dispatcher] 队列已空,停止发送');
|
||||
|
||||
// final devicesCubit = sl<DevicesCubit>();
|
||||
devicesCubit.finishWork();
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
devicesCubit.resetWorkStatus();
|
||||
});
|
||||
// 手动停止
|
||||
routePlanningRepository.stopRoutePlanning();
|
||||
debugPrint('⚠️ [Dispatcher] 队列已空,发送停止指令已发送');
|
||||
return false;
|
||||
}
|
||||
routePlanningRepository.startRoutePlanning(queue);
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('⚠️ [Dispatcher] 状态位异常:${fullData.length > 5 ? fullData[5].toRadixString(16) : "N/A"}');
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ [Dispatcher] 协议头验证失败');
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// 监听特定状态的回复(可复用)
|
||||
/// [commandCode] 指令码
|
||||
/// [statusIndex] 状态字节在 payload 中的索引
|
||||
/// [expectedStatus] 期望的状态值
|
||||
Stream<RawPacket> onStatusResponse({
|
||||
/* Stream<RawPacket> onStatusResponse({
|
||||
required int commandCode,
|
||||
required int statusIndex,
|
||||
required int expectedStatus,
|
||||
@@ -195,5 +316,138 @@ class NetMessageDispatcher {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}*/
|
||||
|
||||
/// 监听特定状态的回复(ack握手机制)
|
||||
// 🔥 新增:ACK 握手机制 (新模式)
|
||||
/// 监听路径规划指令应答,实现完整的 ACK 握手机制
|
||||
/// 协议格式:AB AA 01 [DATA] CRC(2) AA AB
|
||||
/// 上传协议(0x01 路径规划上传):
|
||||
/// - Byte 3: pointCounts (点编号)
|
||||
/// - Byte 4: 未使用
|
||||
/// - Byte 5: 状态
|
||||
/// - 0x01: 已到达
|
||||
/// - 0x02: 收到指令
|
||||
Stream<RawPacket> onPathPlanningResponseWithAck() {
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
|
||||
debugPrint('[Dispatcher-ACK] 开始监听路径规划指令应答 (CMD: 0x01) - ACK 握手机制已启用');
|
||||
|
||||
return onCommand(0x01).where((packet) {
|
||||
// 修改:使用回调检查 AppState
|
||||
if (getAppState != null) {
|
||||
final currentState = getAppState!();
|
||||
if (currentState.toString() == 'AppState.none') {
|
||||
debugPrint('️ [Dispatcher-ACK] 当前 AppState 为 none,停止处理路径规划指令');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 构造完整数据包用于验证
|
||||
final fullData = <int>[
|
||||
0xAB, 0xAA, 0x01,
|
||||
...packet.payload,
|
||||
0x00, 0x00, // CRC 占位
|
||||
0xAA, 0xAB
|
||||
];
|
||||
|
||||
// 验证头部协议
|
||||
if (fullData[0] == 0xAB &&
|
||||
fullData[1] == 0xAA &&
|
||||
fullData[2] == 0x01) {
|
||||
|
||||
debugPrint('[Dispatcher-ACK] 收到路径规划应答 - 完整数据包:${fullData.join(" ")}');
|
||||
|
||||
// 🔥 检查数据包长度是否足够
|
||||
if (fullData.length < 6) {
|
||||
debugPrint('❌ [Dispatcher-ACK] 数据包长度不足,无法解析状态位');
|
||||
return false;
|
||||
}
|
||||
|
||||
final status = fullData[5];
|
||||
final pointIndex = fullData.length > 3 ? fullData[3] : -1;
|
||||
|
||||
debugPrint('📊 [Dispatcher-ACK] 状态位:0x${status.toRadixString(16)}, 点编号:$pointIndex, 期望编号:$_expectedPointIndex');
|
||||
|
||||
// 🔥 核心改进:精准匹配点编号
|
||||
// 如果正在等待 ACK,但收到的点编号不匹配,直接丢弃
|
||||
if ((_waitingForAck || _waitingForArrived) && pointIndex != _expectedPointIndex) {
|
||||
debugPrint('⚠️ [Dispatcher-ACK] 点编号不匹配!收到:$pointIndex, 期望:$_expectedPointIndex,丢弃');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 🔥 ACK 握手机制核心逻辑
|
||||
if (status == 0x02) {
|
||||
// ✅ 下位机回复"收到指令"
|
||||
debugPrint('✅ [Dispatcher-ACK] ACK 确认收到 - 点编号:$pointIndex');
|
||||
_waitingForAck = false;
|
||||
_waitingForArrived = true; // 开始等待"已到达"信号
|
||||
return false; // 继续监听,不返回给流
|
||||
}
|
||||
else if (status == 0x01) {
|
||||
// ✅ 下位机回复"已到达"
|
||||
debugPrint('✅ [Dispatcher-ACK] 已到达确认 - 点编号:$pointIndex');
|
||||
_waitingForArrived = false;
|
||||
|
||||
// 🔥 关键:处理完成后的队列操作
|
||||
final queue = _pathPlanningService.getQueue();
|
||||
|
||||
if (queue.isEmpty) {
|
||||
devicesCubit.finishWork();
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
devicesCubit.resetWorkStatus();
|
||||
});
|
||||
debugPrint('⚠️ [Dispatcher-ACK] 队列已经为空,作业完成');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新已完成的经纬度
|
||||
var deviceAddPathPointModel = queue.first;
|
||||
var d = deviceAddPathPointModel.latitude;
|
||||
var e = deviceAddPathPointModel.longitude;
|
||||
devicesCubit.setArrivedLocation(
|
||||
deviceAddPathPointModel.latitude,
|
||||
deviceAddPathPointModel.longitude
|
||||
);
|
||||
debugPrint(" 准确更新完成的路径点经纬度:$d, $e");
|
||||
|
||||
// 移除已完成的点
|
||||
queue.removeFirst();
|
||||
_pathPlanningService.updateQueue(queue.toList());
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
String timeStr = "${now.hour.toString().padLeft(2, '0')}:"
|
||||
"${now.minute.toString().padLeft(2, '0')}:"
|
||||
"${now.second.toString().padLeft(2, '0')}";
|
||||
|
||||
debugPrint(' $timeStr [Dispatcher-ACK] 准备触发下一个路径点,队列剩余:${queue.length}');
|
||||
|
||||
// 🔥 如果队列已空,停止发送
|
||||
if (queue.isEmpty) {
|
||||
debugPrint('️ [Dispatcher-ACK] 队列已空,停止发送');
|
||||
devicesCubit.finishWork();
|
||||
Future.delayed(Duration(seconds: 1), () {
|
||||
devicesCubit.resetWorkStatus();
|
||||
});
|
||||
routePlanningRepository.stopRoutePlanning();
|
||||
debugPrint('⚠️ [Dispatcher-ACK] 停止指令已发送');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 🔥 发送下一个点(触发 ACK 握手循环)
|
||||
routePlanningRepository.startRoutePlanning(queue);
|
||||
return false; // 不返回给流,避免重复处理
|
||||
}
|
||||
else {
|
||||
debugPrint('⚠️ [Dispatcher-ACK] 未知状态位:0x${status.toRadixString(16)}');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
debugPrint(' [Dispatcher-ACK] 协议头验证失败');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/env/path_planning_config.dart';
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../domain/repositories/route_planning_repository.dart';
|
||||
import '../models/route_plan_send_entity.dart';
|
||||
@@ -8,6 +12,7 @@ import '../models/route_plan_send_entity.dart';
|
||||
class RoutePlanningRepositoryImpl implements RoutePlanningRepository {
|
||||
final TcpClient tcp;
|
||||
late PathPlanner _planner;
|
||||
|
||||
// 🔥 新增:存储 AppState 的引用
|
||||
dynamic _appState;
|
||||
|
||||
@@ -50,15 +55,26 @@ class RoutePlanningRepositoryImpl implements RoutePlanningRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// 全局开关:控制使用哪套路径规划逻辑
|
||||
// true = 使用新的 ACK 握手机制 (将来)
|
||||
// false = 使用现有逻辑 (现在)
|
||||
class PathPlanningMode {
|
||||
static bool useAckHandshake = false; // 默认使用现有逻辑
|
||||
}
|
||||
|
||||
// PathPlanner
|
||||
class PathPlanner {
|
||||
final Queue<DeviceAddPathPointModel> locationQueue = Queue();
|
||||
final TcpClient tcpClient;
|
||||
bool isStart = false;
|
||||
|
||||
// 🔥 新增:作业控制状态
|
||||
// 新增:作业控制状态
|
||||
bool _isPaused = false;
|
||||
bool _isStopped = false;
|
||||
// 新增:ACK 握手状态 (仅新模式使用)
|
||||
bool _waitingForAck = false; // 是否正在等待 ACK 确认
|
||||
int _currentPointIndex = 0; // 🔥 当前发送的点编号(从 0 开始递增)
|
||||
|
||||
|
||||
PathPlanner(this.tcpClient);
|
||||
|
||||
@@ -70,7 +86,7 @@ class PathPlanner {
|
||||
// tcpClient.disconnect();
|
||||
// return;
|
||||
// }
|
||||
// 🔥 关键检查:暂停或停止时不再发送
|
||||
// 关键检查:暂停或停止时不再发送
|
||||
if (_isPaused) {
|
||||
print("⏸️ 当前处于暂停状态,停止发送指令");
|
||||
return;
|
||||
@@ -136,7 +152,15 @@ class PathPlanner {
|
||||
return;
|
||||
}
|
||||
print("[发送指令要转换类型了完成]");
|
||||
sendNextLocation();
|
||||
// sendNextLocation();
|
||||
// 🔥 根据全局开关选择使用哪套发送逻辑
|
||||
if (PathPlanningConfig.useAckHandshake) {
|
||||
print("🔥 使用 ACK 握手机制发送路径点");
|
||||
sendNextLocationWithAck(sl<NetMessageDispatcher>());
|
||||
} else {
|
||||
print("🔥 使用现有逻辑发送路径点");
|
||||
sendNextLocation();
|
||||
}
|
||||
|
||||
}
|
||||
/// 暂停
|
||||
@@ -170,9 +194,17 @@ class PathPlanner {
|
||||
tcpClient.sendDeviceStateChange(entity);
|
||||
|
||||
// 恢复后继续发送下一个点
|
||||
// if (locationQueue.isNotEmpty) {
|
||||
// print("📍 恢复发送下一个路径点");
|
||||
// sendNextLocation();
|
||||
// }
|
||||
if (locationQueue.isNotEmpty) {
|
||||
print("📍 恢复发送下一个路径点");
|
||||
sendNextLocation();
|
||||
if (PathPlanningConfig.useAckHandshake) {
|
||||
sendNextLocationWithAck(sl<NetMessageDispatcher>());
|
||||
} else {
|
||||
sendNextLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 停止
|
||||
@@ -199,7 +231,55 @@ class PathPlanner {
|
||||
tcpClient.sendDeviceStateChange(entity);
|
||||
}
|
||||
|
||||
// 🔥 新增:ACK 握手机制的发送方法 (新模式)
|
||||
void sendNextLocationWithAck(NetMessageDispatcher dispatcher) {
|
||||
print("[在发送指令 sendNextLocationWithAck 方法中 - ACK 模式]");
|
||||
// 🔥 关键检查:如果正在等待 ACK,不要重复发送
|
||||
if (_waitingForAck) {
|
||||
print("⏳ 正在等待 ACK 确认,跳过发送");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 关键检查:暂停或停止时不再发送
|
||||
if (_isPaused) {
|
||||
print("⏸️ 当前处于暂停状态,停止发送指令");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isStopped || (isStart && locationQueue.isEmpty)) {
|
||||
isStart = false;
|
||||
if (_isStopped) {
|
||||
print("️ 已停止作业,清空队列");
|
||||
locationQueue.clear(); // 清空剩余队列
|
||||
} else {
|
||||
print("[路径点发送完毕]");
|
||||
}
|
||||
_isStopped = false; // 重置停止标志
|
||||
return;
|
||||
}
|
||||
|
||||
final entity = locationQueue.removeFirst(); // 类型:DeviceAddPathPointModel
|
||||
|
||||
// 🔥 核心改进:使用递增的点编号,确保精准匹配
|
||||
final pointIndex = _currentPointIndex++;
|
||||
|
||||
final routePlanSendEntity = RoutePlanSendEntity(
|
||||
commandType: 0x01,
|
||||
pointCounts: pointIndex, // 🔥 使用点编号,而不是固定的 1
|
||||
targetLatitude: entity.latitude,
|
||||
targetLongitude: entity.longitude,
|
||||
speed: 1000,
|
||||
);
|
||||
|
||||
print("====Lat:${entity.latitude} ====Lng:${entity.longitude}");
|
||||
tcpClient.sendPathPoint( routePlanSendEntity);
|
||||
isStart = true;
|
||||
|
||||
// 🔥 关键:标记为正在等待 ACK
|
||||
_waitingForAck = true;
|
||||
dispatcher.setExpectedPointIndex(pointIndex); // 🔥 通知 Dispatcher
|
||||
print(" 已锁定发送,等待 ACK 确认 (0x02)...");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class CenterControlArea extends StatelessWidget {
|
||||
width: expandedWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [_buildSliderBox(expandedWidth, "备用", false)],
|
||||
children: [_buildSliderBox(expandedWidth, "割刀", false)],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -72,7 +72,8 @@ class MyApp extends StatelessWidget {
|
||||
),
|
||||
// 其他 Cubit...
|
||||
],
|
||||
child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>(),
|
||||
child: MaterialApp.router(title: 'Maibu Satabot',
|
||||
theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>(),
|
||||
builder: (context, child) {
|
||||
return _LifecycleListener(child: child);
|
||||
},),
|
||||
|
||||
Reference in New Issue
Block a user