arduino避障小车阶段性开发

This commit is contained in:
2025-05-30 19:33:46 +08:00
parent d0a550cab6
commit 2aa0c4ffde
3 changed files with 240 additions and 6 deletions

View File

@@ -77,3 +77,45 @@
* **首要任务是修正单片机固件的ACK发送逻辑**,确保与后端正确对接。 * **首要任务是修正单片机固件的ACK发送逻辑**,确保与后端正确对接。
* 在完成固件修改后建议进行完整的MQTT通信测试覆盖所有指令类型及其ACK流程。 * 在完成固件修改后建议进行完整的MQTT通信测试覆盖所有指令类型及其ACK流程。
## 2025-05-24: 后端会话管理核心逻辑强化
**主要变更与决策:**
1. **明确用户充电请求流程与并发处理:**
* 用户请求充电时 (`ChargingSessionController.requestCharging` -> `ChargingSessionServiceImpl.requestCharging` -> `assignRobotToSession`),系统会检查车位可用性、用户状态,并尝试分配空闲机器人。
* **机器人分配机制**: `ChargingRobotServiceImpl.assignIdleRobot()` 负责查找空闲机器人并将其状态更新为 `MOVING` (或类似表示已分配的状态)。此操作与会话创建、车位预定、任务创建均在一个事务内。
* **机器人繁忙处理**: 如果没有空闲机器人,`assignRobotToSession` 将返回 `null`,随后 `requestCharging` 方法会将此会话标记为 `CANCELLED_BY_SYSTEM` 并向用户抛出明确的"无可用机器人"或"机器人繁忙"的错误。**系统不会为此类请求创建等待执行的 `RobotTask`**,用户需稍后重试。这简化了后端的任务管理和状态维护。
2. **MQTT 指令发送与任务状态同步 (`MqttServiceImpl.sendCommand`):**
* **任务预创建**: `sendCommand` 方法要求调用者 (如 `ChargingSessionServiceImpl`) 必须预先创建状态为 `PENDING``RobotTask`并将任务ID (`associatedTaskId`) 传递给 `sendCommand`
* **机器人繁忙校验**: 在发送指令前,`sendCommand` 会调用 `RobotTaskServiceImpl.hasOtherPendingOrSentTask()` 检查目标机器人是否已有其他 `PENDING``SENT` 状态的任务(排除当前 `associatedTaskId`)。若是,则通常中止发送(`STOP_CHARGE` 指令除外,它具有优先权)。
* **Payload 增强**: `sendCommand` 会自动向发送的MQTT JSON payload 中添加 `taskId` 字段,其值为 `associatedTaskId`。这有助于机器人端将ACK消息与特定任务关联。
* **事务与数据一致性**: `sendCommand` 标记为 `@Transactional`
* MQTT消息成功发布后会调用 `RobotTaskServiceImpl.markTaskAsSent()` 将对应任务状态从 `PENDING` 更新为 `SENT`
* 为确保数据一致性避免MQTT已发送但任务状态更新失败`RobotTaskServiceImpl.markTaskAsSent()` 方法已被修改:**在其无法成功将任务标记为 SENT如任务不存在、原状态非PENDING、数据库更新失败会抛出 `BusinessException`**。此异常会触发 `sendCommand` 所在事务的回滚从而撤销MQTT发送如果尚未实际完成网络传输或至少确保数据库层面的一致性。
3. **会话状态流转与MQTT消息处理 (初步对齐):**
* **"到达即充电"逻辑**: 当前遵循此简化逻辑。当 `MOVE_TO_SPOT` 任务成功被机器人ACK后`ChargingSessionServiceImpl.handleRobotArrival()` 会直接将会话状态更新为 `CHARGING_STARTED`
* `MqttMessageHandler` 在收到机器人的状态回报(特别是包含 `taskId` 的ACK会首先调用 `RobotTaskService` 更新任务状态,然后基于任务结果调用 `ChargingSessionService` 的相应方法来驱动会话状态的变更。
4. **兼容性考虑**:
* 当前的修改重点在于增强后端逻辑的健壮性和数据一致性同时尽量维持现有的MQTT消息JSON结构 (仅在payload中增加 `taskId`,这是一个向后兼容的增强) 和主要的业务流程,以避免对前端和单片机代码产生破坏性变更。
* MQTT指令名称 (如 `MOVE` vs `MOVE_TO_SPOT`) 与 `CommandTypeEnum` 的映射关系需要在后端处理,以适应单片机固件的期望。
**后续关注点:**
* 详细实现 `ChargingSessionService` 中处理充电结束、取消、错误、超时的逻辑。
* 设计和实现计费服务。
* 确保 `MqttMessageHandler` 中对所有预期机器人回报的完整处理和对 `ChargingSessionService` 的正确调用。
* 审阅并完善所有相关的 `@Transactional` 注解,确保事务边界和传播行为的正确性。
## YYYY-MM-DD (请替换为今天的实际日期)
- 新建 Arduino Nano 循迹避障项目 `arduino_nano_tracker`
- 目标:实现单片机控制的循迹和避障功能。
- 硬件配置:
- TT 电机 x2 (PWM控制转速): 左电机引脚 9, 右电机引脚 10。
- TRCT5000 循迹传感器 x2: 左传感器引脚 7, 右传感器引脚 8。
- 超声波传感器 (HC-SR04): Trig 引脚 12, Echo 引脚 11。
- 180度舵机 (SG90): 引脚 3。
- 在 `arduino_nano_tracker/` 目录下创建了 `arduino_nano_tracker.ino` 文件,并包含基础的引脚定义和 `setup()`/`loop()` 结构。

View File

@@ -0,0 +1,184 @@
#include <Servo.h> // 舵机库
// 引脚定义
// 电机
const int LEFT_MOTOR_PIN = 9; // 左电机PWM控制引脚
const int RIGHT_MOTOR_PIN = 10; // 右电机PWM控制引脚
// TRCT5000 循迹传感器
const int LEFT_TRCT_PIN = 7; // 左循迹传感器引脚
const int RIGHT_TRCT_PIN = 8; // 右循迹传感器引脚
// 超声波传感器
const int TRIG_PIN = 12; // 超声波Trig引脚
const int ECHO_PIN = 11; // 超声波Echo引脚
// 舵机
const int SERVO_PIN = 3; // 舵机控制引脚
Servo steeringServo; // 创建舵机对象
// 常量定义
const int OBSTACLE_DISTANCE_THRESHOLD_CM = 20; // 障碍物检测阈值 (厘米)
const int SERVO_NEUTRAL_ANGLE = 90; // 舵机中间位置角度
const int SERVO_LEFT_ANGLE = 75; // 舵机微调左转角度 (原为60, 调整为更小角度)
const int SERVO_RIGHT_ANGLE = 105; // 舵机微调右转角度 (原为120, 调整为更小角度)
// 电机速度 (analogWrite PWM值范围 0-255) - 这些常量在新的电机控制逻辑下意义减弱
const int MOTOR_STATE_STOP = LOW; // 电机停止状态 (使用LOW)
const int MOTOR_STATE_RUN = HIGH; // 电机运行状态 (使用HIGH)
// 保留旧的常量名以便于理解之前的逻辑但实际传递给电机控制函数的值会被转换为HIGH/LOW
const int MOTOR_SPEED_STOP = 0; // 逻辑上的停止 (会转换为LOW)
const int MOTOR_SPEED_NORMAL = 1; // 逻辑上的运行 (会转换为HIGH)
const int MOTOR_SPEED_SLOW = 1; // 逻辑上的运行 (会转换为HIGH)
const int MOTOR_SPEED_TURN_ASSIST = 1; // 逻辑上的运行 (会转换为HIGH)
// const int MOTOR_SPEED_DIFFERENTIAL_SLOW = 70; // 这个在单边驱动时不再直接使用
// TRCT5000 传感器逻辑 (假设检测到黑线为低电平, 白色地面为高电平)
// 具体根据你的传感器模块实际输出进行调整 (例如,如果模块输出反相,则需要调换定义)
#define ON_LINE LOW // 在线上 (检测到黑线)
#define OFF_LINE HIGH // 不在线上 (检测到白色地面)
void setup() {
// 初始化串口通信波特率9600
Serial.begin(9600);
while (!Serial) {
; // 等待串口连接 (对于某些板子如Leonardo是必需的Nano通常不需要但加上无害)
}
Serial.println("串口已连接。");
// 初始化电机引脚为输出模式
pinMode(LEFT_MOTOR_PIN, OUTPUT);
pinMode(RIGHT_MOTOR_PIN, OUTPUT);
// 初始化TRCT5000循迹传感器引脚为输入模式
pinMode(LEFT_TRCT_PIN, INPUT);
pinMode(RIGHT_TRCT_PIN, INPUT);
// 初始化超声波传感器引脚
pinMode(TRIG_PIN, OUTPUT); // Trig引脚为输出
pinMode(ECHO_PIN, INPUT); // Echo引脚为输入
// 舵机初始化连接到SERVO_PIN并设置到中间位置
steeringServo.attach(SERVO_PIN);
steeringServo.write(SERVO_NEUTRAL_ANGLE);
// 确保初始状态电机停止
stopMotors();
Serial.println("初始状态:电机已停止。");
Serial.println("Arduino Nano 循迹避障小车 初始化完成,准备就绪。");
Serial.println("-----------------------------------------");
}
void loop() {
Serial.println("--- 开始新循环 ---");
long distance = readUltrasonicDistance(); // 读取超声波测量的距离
Serial.print("超声波距离: "); Serial.print(distance); Serial.println(" cm");
if (distance > 0 && distance < OBSTACLE_DISTANCE_THRESHOLD_CM) {
// 如果检测到障碍物 (距离大于0且小于阈值)
Serial.println("状态更新:检测到障碍物! 执行停止。");
stopMotors(); // 停止电机
steeringServo.write(SERVO_NEUTRAL_ANGLE); // 舵机回中
Serial.println("动作:舵机回中。");
} else {
Serial.println("状态更新:无障碍物,执行循迹逻辑。");
trackLine();
}
Serial.println("--- 循环结束,延时 ---");
delay(100); // 稍微增加延时方便观察串口输出原为50
}
// --- 电机控制函数 ---
void controlMotors(int leftState, int rightState) {
// 直接使用digitalWrite控制电机HIGH为全速LOW为停止
// leftState 和 rightState 现在应该代表 MOTOR_STATE_RUN (HIGH) 或 MOTOR_STATE_STOP (LOW)
digitalWrite(LEFT_MOTOR_PIN, leftState);
digitalWrite(RIGHT_MOTOR_PIN, rightState);
Serial.print("电机控制(直接驱动):左轮状态=");
Serial.print(leftState == HIGH ? "运行(HIGH)" : "停止(LOW)");
Serial.print(", 右轮状态=");
Serial.println(rightState == HIGH ? "运行(HIGH)" : "停止(LOW)");
}
void moveForward() { // 不再需要speed参数因为总是全速
Serial.println("动作:向前行驶 (电机全速)。");
steeringServo.write(SERVO_NEUTRAL_ANGLE); // 确保舵机在中间位置 (直行)
Serial.println("动作:舵机回中 (配合直行)。");
controlMotors(MOTOR_STATE_RUN, MOTOR_STATE_RUN); // 设置左右电机运行
}
void stopMotors() {
Serial.println("动作:停止电机。");
controlMotors(MOTOR_STATE_STOP, MOTOR_STATE_STOP); // 设置左右电机停止
}
void turnLeft() { // 不再需要turnSpeed参数
Serial.println("动作:左转 (电机全速,舵机控制)。");
steeringServo.write(SERVO_LEFT_ANGLE); // 舵机打到左转角度
Serial.print("动作:舵机打到左转角度 ("); Serial.print(SERVO_LEFT_ANGLE); Serial.println("度)。");
// 两个电机都保持运行,转向仅靠舵机
controlMotors(MOTOR_STATE_RUN, MOTOR_STATE_RUN);
}
void turnRight() { // 不再需要turnSpeed参数
Serial.println("动作:右转 (电机全速,舵机控制)。");
steeringServo.write(SERVO_RIGHT_ANGLE); // 舵机打到右转角度
Serial.print("动作:舵机打到右转角度 ("); Serial.print(SERVO_RIGHT_ANGLE); Serial.println("度)。");
// 两个电机都保持运行,转向仅靠舵机
controlMotors(MOTOR_STATE_RUN, MOTOR_STATE_RUN);
}
// --- 传感器读取函数 ---
long readUltrasonicDistance() {
// 读取超声波传感器距离 (返回厘米cm)
// 清除TRIG_PIN的任何先前状态确保低电平
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
// 发送一个10微秒的高电平脉冲到TRIG_PIN以触发超声波发送
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// 读取ECHO_PIN返回的脉冲持续时间 (单位:微秒)
// pulseIn函数会等待引脚变为高电平然后开始计时再等待引脚变为低电平停止计时。
long duration = pulseIn(ECHO_PIN, HIGH);
// 计算距离 (单位:厘米)
// 声速 = 343米/秒 = 0.0343 厘米/微秒
// 距离 = (时间 * 声速) / 2 (因为声波是往返的)
long distance = duration * 0.0343 / 2;
// Serial.print("[内部] 超声波原始时长: "); Serial.println(duration);
return distance;
}
void trackLine() {
// 循迹逻辑
int leftSensorVal = digitalRead(LEFT_TRCT_PIN); // 读取左循迹传感器状态
int rightSensorVal = digitalRead(RIGHT_TRCT_PIN); // 读取右循迹传感器状态
Serial.print("循迹传感器状态: 左=");
Serial.print(leftSensorVal == ON_LINE ? "线上(LOW)" : "线下(HIGH)");
Serial.print(", 右=");
Serial.println(rightSensorVal == ON_LINE ? "线上(LOW)" : "线下(HIGH)");
// 新的循迹逻辑:默认前进,检测到线时微调方向 (电机始终全速)
if (leftSensorVal == ON_LINE && rightSensorVal == OFF_LINE) {
Serial.println("循迹判断:左线上,右线下 -> 执行左转 (电机保持运行)");
turnLeft();
} else if (leftSensorVal == OFF_LINE && rightSensorVal == ON_LINE) {
Serial.println("循迹判断:左线下,右线上 -> 执行右转 (电机保持运行)");
turnRight();
} else if (leftSensorVal == ON_LINE && rightSensorVal == ON_LINE) {
Serial.println("循迹判断:两侧均线上 -> 执行直行 (电机保持运行)");
moveForward(); // 两侧都压线,也直行
} else { // leftSensorVal == OFF_LINE && rightSensorVal == OFF_LINE
Serial.println("循迹判断:两侧均线下 -> 执行直行 (电机保持运行)");
moveForward();
}
}

View File

@@ -2,6 +2,8 @@ package com.yupi.project.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yupi.project.common.ErrorCode;
import com.yupi.project.exception.BusinessException;
import com.yupi.project.model.entity.RobotTask; import com.yupi.project.model.entity.RobotTask;
import com.yupi.project.model.enums.CommandTypeEnum; import com.yupi.project.model.enums.CommandTypeEnum;
import com.yupi.project.model.enums.RobotTaskStatusEnum; import com.yupi.project.model.enums.RobotTaskStatusEnum;
@@ -97,17 +99,21 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
public boolean markTaskAsSent(Long taskId, Date sentTime) { public boolean markTaskAsSent(Long taskId, Date sentTime) {
if (taskId == null || sentTime == null) { if (taskId == null || sentTime == null) {
log.error("Cannot mark task as sent: taskId or sentTime is null."); log.error("Cannot mark task as sent: taskId or sentTime is null.");
return false; // 抛出异常而不是返回false
throw new BusinessException(ErrorCode.PARAMS_ERROR, "Task ID or sentTime cannot be null when marking task as SENT.");
} }
RobotTask task = this.getById(taskId); RobotTask task = this.getById(taskId);
if (task == null) { if (task == null) {
log.warn("Cannot mark task as sent: Task with ID {} not found.", taskId); log.warn("Cannot mark task as sent: Task with ID {} not found.", taskId);
return false; // 抛出异常
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "Task with ID " + taskId + " not found.");
} }
if (task.getStatus() != RobotTaskStatusEnum.PENDING) { if (task.getStatus() != RobotTaskStatusEnum.PENDING) {
log.warn("Cannot mark task {} as SENT. Current status is {}, expected PENDING.", taskId, task.getStatus()); log.warn("Cannot mark task {} as SENT. Current status is {}, expected PENDING.", taskId, task.getStatus());
return false; // 抛出异常
throw new BusinessException(ErrorCode.OPERATION_ERROR,
"Task " + taskId + " is in status " + task.getStatus() + ", expected PENDING to mark as SENT.");
} }
RobotTask updateTask = new RobotTask(); RobotTask updateTask = new RobotTask();
@@ -119,9 +125,11 @@ public class RobotTaskServiceImpl extends ServiceImpl<RobotTaskMapper, RobotTask
if (updated) { if (updated) {
log.info("Marked RobotTask with ID: {} as SENT at {}.", taskId, sentTime); log.info("Marked RobotTask with ID: {} as SENT at {}.", taskId, sentTime);
} else { } else {
log.error("Failed to mark RobotTask with ID: {} as SENT.", taskId); log.error("Failed to mark RobotTask with ID: {} as SENT. Update to database failed.", taskId);
// 抛出异常
throw new BusinessException(ErrorCode.OPERATION_ERROR, "Failed to update task " + taskId + " to SENT in database.");
} }
return updated; return true; // 如果没有抛出异常,则表示成功
} }
@Override @Override