修复充电

This commit is contained in:
2025-06-07 21:37:31 +08:00
parent fb27dc6f29
commit 20e35b3fd1
6 changed files with 78 additions and 253 deletions

View File

@@ -151,4 +151,8 @@
- 舵机MG995
- 连接引脚:舵机信号线连接到 Nano 的 D9 PWM 引脚。
-`mg995_nano_test/` 目录下创建了 `mg995_nano_test.ino` 文件,使用标准 `Servo.h` 库实现舵机往复运动。
- **重要提示**:同样需要为 MG995 舵机提供独立的 5V 外部电源,并与 Nano 共地。
- **重要提示**:同样需要为 MG995 舵机提供独立的 5V 外部电源,并与 Nano 共地。
## YYYY-MM-DD (请替换为今天的实际日期)
- **Fix:** 修复了在充电会话结束时,由于在 `calculateCostAndFinalizeSession` 方法中存在冗余的车位状态检查和更新逻辑导致在特定事务场景下产生非法SQL语句 (`UPDATE parking_spot` 缺少 `SET` 子句) 的严重BUG。
- **Refactor:** 重构了 `ChargingSessionServiceImpl`,将车位释放的逻辑完全保留在 `handleChargingEnd` 方法中,确保 `calculateCostAndFinalizeSession` 只负责计费,遵循单一职责原则,提高了代码的健壮性和可维护性。

View File

@@ -1,184 +0,0 @@
#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

@@ -227,14 +227,27 @@ public class ChargingRobotServiceImpl extends ServiceImpl<ChargingRobotMapper, C
}
@Override
@Transactional
public boolean releaseRobot(Long robotId) {
ChargingRobot robot = getById(robotId);
if (robot == null) {
log.warn("尝试释放机器人失败,机器人不存在: {}", robotId);
return false;
}
// 将机器人状态设置为空闲并清除当前任务ID
return updateRobotStatus(robot.getRobotUid(), RobotStatusEnum.IDLE, null, null, null, null); // 第二个参数传null以清除任务ID
// 关键修复:确保释放机器人时,原子性地更新其状态和任务ID
ChargingRobot robotToUpdate = new ChargingRobot();
robotToUpdate.setId(robotId);
robotToUpdate.setStatus(RobotStatusEnum.IDLE.getValue());
robotToUpdate.setCurrentTaskId(null); // 显式清除任务ID
boolean success = this.updateById(robotToUpdate);
if (success) {
log.info("成功释放机器人 {}状态设置为IDLE任务ID已清除。", robot.getRobotUid());
} else {
log.error("数据库操作失败,未能释放机器人 {}", robot.getRobotUid());
}
return success;
}
@Override

View File

@@ -352,24 +352,6 @@ public class ChargingSessionServiceImpl extends ServiceImpl<ChargingSessionMappe
log.info("会话 {} 费用计算完成: {}元. 状态更新为 PAYMENT_PENDING", sessionId, cost);
// 此处可以触发通知用户支付
}
// 在计费完成后如果会话已标记为COMPLETED但尚未支付并且车位状态不是AVAILABLE则将其设置为空闲
// 这主要覆盖了 handleChargingEnd 中可能未完全释放车位的情况(例如,如果计费失败)
// 或者如果 calculateCostAndFinalizeSession 是独立调用的。
String sessionStatus = session.getStatus();
String paymentStatus = session.getPaymentStatus();
if (ChargingSessionStatusEnum.CHARGING_COMPLETED.getValue().equals(sessionStatus) ||
PaymentStatusEnum.PAID.getValue().equals(paymentStatus) ||
PaymentStatusEnum.FAILED.getValue().equals(paymentStatus)) {
ParkingSpot spot = parkingSpotService.getById(session.getSpotId());
if (spot != null && !ParkingSpotStatusEnum.AVAILABLE.getValue().equals(spot.getStatus())) {
log.info("在 finalizeSession for session {} 时,车位 {} (UID: {}) 状态为 {},将其更新为 AVAILABLE。",
sessionId, spot.getId(), spot.getSpotUid(), spot.getStatus());
parkingSpotService.updateSpotStatus(spot.getSpotUid(), ParkingSpotStatusEnum.AVAILABLE, null);
}
}
return updated;
}

View File

@@ -68,34 +68,34 @@ public class MqttMessageHandler implements IMqttMessageListener {
statusMessage.getStatus(), statusMessage.getTaskId(), robotUIDFromTopicSource, payloadJson);
// Do not return, general status might still need processing.
} else {
boolean taskUpdated; // Defined outside switch
switch (taskNewStatus) {
case PROCESSING:
taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case COMPLETED:
taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case FAILED:
String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ?
statusMessage.getErrorCode() + ": " + statusMessage.getMessage() :
statusMessage.getMessage();
taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date());
break;
default:
log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.",
taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource);
boolean taskUpdated; // Defined outside switch
switch (taskNewStatus) {
case PROCESSING:
taskUpdated = robotTaskService.markTaskAsProcessing(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case COMPLETED:
taskUpdated = robotTaskService.markTaskAsCompleted(statusMessage.getTaskId(), new Date(), statusMessage.getMessage());
break;
case FAILED:
String combinedTaskErrorMessage = statusMessage.getErrorCode() != null ?
statusMessage.getErrorCode() + ": " + statusMessage.getMessage() :
statusMessage.getMessage();
taskUpdated = robotTaskService.markTaskAsFailed(statusMessage.getTaskId(), combinedTaskErrorMessage, new Date());
break;
default:
log.warn("Received unhandled RobotTaskStatusEnum: {} for task {} from robot {}. Ignoring.",
taskNewStatus, statusMessage.getTaskId(), robotUIDFromTopicSource);
taskUpdated = false; // To prevent logging success
break;
}
if (taskUpdated) {
log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.",
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource);
} else {
log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}",
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage());
}
}
if (taskUpdated) {
log.info("Successfully updated task {} to status {} for robot {} based on MQTT message.",
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource);
} else {
log.warn("Failed to update task {} to status {} for robot {} (or task not found/invalid state transition). Message: {}",
statusMessage.getTaskId(), taskNewStatus, robotUIDFromTopicSource, statusMessage.getMessage());
}
}
}
// Scenario 2: Message contains a general robot status update.

View File

@@ -127,41 +127,51 @@ public class ParkingSpotServiceImpl extends ServiceImpl<ParkingSpotMapper, Parki
return false;
}
ParkingSpot parkingSpot = this.findBySpotUid(spotUID);
ParkingSpot parkingSpot = this.findBySpotUid(spotUID);
if (parkingSpot == null) {
log.warn("更新车位状态失败:未找到 UID 为 {} 的车位。", spotUID);
return false;
}
ParkingSpot spotToUpdate = new ParkingSpot();
spotToUpdate.setId(parkingSpot.getId());
boolean changed = false;
// Compare enum with String value from entity
if (newStatus != null && !newStatus.getValue().equals(parkingSpot.getStatus())) {
spotToUpdate.setStatus(newStatus.getValue()); // Set String value to entity
changed = true;
// 彻底重构:创建一个包含所有最终状态的更新对象,避免复杂的条件逻辑。
// 这是解决顽固的 "empty set clause" 问题的最稳健方法。
boolean needsUpdate = false;
// 检查状态是否需要更新
if (!newStatus.getValue().equals(parkingSpot.getStatus())) {
needsUpdate = true;
}
if (currentSessionId == null && parkingSpot.getCurrentSessionId() != null) {
spotToUpdate.setCurrentSessionId(null);
changed = true;
} else if (currentSessionId != null && !currentSessionId.equals(parkingSpot.getCurrentSessionId())) {
// 检查会话ID是否需要更新
Long targetSessionId = (newStatus == ParkingSpotStatusEnum.AVAILABLE) ? null : currentSessionId;
if (targetSessionId == null && parkingSpot.getCurrentSessionId() != null) {
needsUpdate = true;
} else if (targetSessionId != null && !targetSessionId.equals(parkingSpot.getCurrentSessionId())) {
needsUpdate = true;
}
if (!needsUpdate) {
log.info("车位 {} 的状态与数据库一致 (状态: {}, 会话ID: {}),无需更新。",
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
return true; // 数据一致,操作成功
}
// 创建一个完整的更新实体
ParkingSpot spotToUpdate = new ParkingSpot();
spotToUpdate.setId(parkingSpot.getId());
spotToUpdate.setStatus(newStatus.getValue());
if (newStatus == ParkingSpotStatusEnum.AVAILABLE) {
spotToUpdate.setCurrentSessionId(null);
} else {
spotToUpdate.setCurrentSessionId(currentSessionId);
changed = true;
}
if (!changed) {
log.info("车位 {} 的状态与数据库一致 (状态: {}, 会话ID: {}),无需更新。",
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
return true; // Data is consistent, operation considered successful
}
// updateTime will be auto-filled
boolean success = this.updateById(spotToUpdate);
if (success) {
log.info("成功更新车位 {} 的状态。新状态: {}, 当前会话ID: {}",
spotUID, newStatus, currentSessionId == null ? "null" : currentSessionId);
log.info("成功更新车位 {} 的状态。新状态: {}, 当前会话ID: {}",
spotUID, spotToUpdate.getStatus(), spotToUpdate.getCurrentSessionId() == null ? "null" : spotToUpdate.getCurrentSessionId());
} else {
log.error("更新车位 {} 的状态失败 (数据库操作失败)。", spotUID);
}