充电和脱离

This commit is contained in:
2026-09-18 16:51:03 +08:00
parent 0bf52a5d55
commit fe56a8ba6d
7 changed files with 768 additions and 13 deletions

View File

@@ -70,6 +70,26 @@ export interface RosCar {
liftHeight?: number;
/** 升降柱状态 */
lift?: RosLift;
/** 充电点 X 坐标 */
dockX?: number;
/** 充电点 Y 坐标 */
dockY?: number;
/** 充电点朝向 */
dockYaw?: number;
/** 预对接点 X 坐标 */
preDockX?: number;
/** 预对接点 Y 坐标 */
preDockY?: number;
/** 预对接点朝向 */
preDockYaw?: number;
/** 是否开启自动回充 */
autoRechargeEnabled?: boolean;
/** 自动回充低电量阈值(%) */
lowBatteryThreshold?: number;
/** 自动回充状态文本 */
dockStatus?: string;
/** 自动回充是否进行中 */
dockActive?: boolean;
/** 创建时间 */
createTime?: string;
/** 更新时间 */
@@ -293,3 +313,61 @@ export function updateRosCarMapUrls(data: {
data,
});
}
/**
* 小车充电点与自动回充配置更新参数
*/
export interface RosCarDockConfigUpdate {
/** 主键id */
id: string;
/** 充电点 X 坐标 */
dockX?: number;
/** 充电点 Y 坐标 */
dockY?: number;
/** 充电点朝向 */
dockYaw?: number;
/** 预对接点 X 坐标 */
preDockX?: number;
/** 预对接点 Y 坐标 */
preDockY?: number;
/** 预对接点朝向 */
preDockYaw?: number;
/** 是否开启自动回充 */
autoRechargeEnabled?: boolean;
/** 自动回充低电量阈值(%) */
lowBatteryThreshold?: number;
}
// 更新小车充电点与自动回充配置
export function updateRosCarDockConfig(data: RosCarDockConfigUpdate) {
return request<ApiResult<null>>({
url: "/admin/rosCar/updateDockConfig",
data,
});
}
/**
* 手动触发回充结果,对应后端 com.haizhiyustc.ros.dto.RosChargingResultDTO
*/
export interface RosChargingResult {
/** 小车是否接受本次触发 */
accepted?: boolean;
/** 触发结果说明 */
message?: string;
}
// 手动触发小车自动回充
export function startRosCharging(carId: string) {
return request<ApiResult<RosChargingResult>>({
url: "/admin/rosCar/startCharging",
params: { carId },
});
}
// 手动触发小车脱离充电桩
export function undockRosCharging(carId: string) {
return request<ApiResult<RosChargingResult>>({
url: "/admin/rosCar/undockCharging",
params: { carId },
});
}

View File

@@ -2655,6 +2655,58 @@
color: #ffd9d5;
}
.inspection-action-btn.charging {
border-color: rgba(126, 255, 196, 0.32);
background: linear-gradient(90deg, rgba(20, 104, 82, 0.96), rgba(10, 48, 44, 0.98));
color: #d5ffee;
}
.inspection-action-btn.undock {
border-color: rgba(255, 205, 126, 0.32);
background: linear-gradient(90deg, rgba(122, 78, 18, 0.96), rgba(70, 42, 12, 0.98));
color: #ffeccd;
}
.charging-status-row {
display: flex;
align-items: center;
gap: 8px;
min-height: 20px;
}
.charging-status-dot {
width: 8px;
min-width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(140, 180, 200, 0.5);
box-shadow: 0 0 0 3px rgba(140, 180, 200, 0.12);
}
.charging-status-dot.active {
background: #7dffb0;
box-shadow: 0 0 0 3px rgba(125, 255, 176, 0.18), 0 0 12px rgba(125, 255, 176, 0.6);
animation: charging-dot-pulse 1.4s ease-in-out infinite;
}
.charging-status-text {
color: rgba(190, 232, 248, 0.88);
font-size: 12px;
line-height: 1.5;
word-break: break-all;
}
@keyframes charging-dot-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
.inspection-action-btn:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(102, 235, 255, 0.72);

View File

@@ -4,6 +4,7 @@ import {
getRosCarList,
getRosInspectionTaskDetail,
saveRosInspectionTask,
updateRosCarDockConfig,
updateRosCarMapUrls,
uploadFile,
} from "@/api/rosCar";
@@ -19,7 +20,7 @@ type RecognitionActionCode =
| "TUNNEL_WATER"
| "WALL_CRACK";
type PatrolTaskMode = "定时" | "周期" | "临时";
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint";
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint" | "dock" | "preDock";
interface RosCarRecord {
id?: string;
@@ -42,6 +43,14 @@ interface RosCarRecord {
liftHeight?: number;
mapPgmUrl?: string;
mapYamlUrl?: string;
dockX?: number;
dockY?: number;
dockYaw?: number;
preDockX?: number;
preDockY?: number;
preDockYaw?: number;
autoRechargeEnabled?: boolean;
lowBatteryThreshold?: number;
createTime?: string;
updateTime?: string;
}
@@ -67,6 +76,17 @@ interface DeviceRecord {
infraredUrl?: string;
mapPgmUrl?: string;
mapYamlUrl?: string;
poseX?: number;
poseY?: number;
poseYaw?: number;
dockX?: number;
dockY?: number;
dockYaw?: number;
preDockX?: number;
preDockY?: number;
preDockYaw?: number;
autoRechargeEnabled?: boolean;
lowBatteryThreshold?: number;
createdAt: string;
}
@@ -190,6 +210,17 @@ interface PatrolTask {
updatedAt: string;
}
interface DockConfigState {
dockX?: number;
dockY?: number;
dockYaw?: number;
preDockX?: number;
preDockY?: number;
preDockYaw?: number;
autoRechargeEnabled: boolean;
lowBatteryThreshold: number;
}
interface DeviceConfigState {
maps: MapRecord[];
activeMapId: string;
@@ -201,6 +232,7 @@ interface DeviceConfigState {
points: PatrolPoint[];
selectedPointIds: string[];
plans: PatrolTask[];
dock: DockConfigState;
}
interface PlanFormState {
@@ -243,6 +275,8 @@ let patrolActionIdSeed = 0;
const editorModes: Array<{ id: EditorMode; label: string }> = [
{ id: "normal", label: "新增业务点" },
{ id: "waypoint", label: "新增途经点" },
{ id: "dock", label: "设置充电点" },
{ id: "preDock", label: "设置预对接点" },
];
const devices = ref<DeviceRecord[]>([]);
@@ -371,6 +405,7 @@ const rosMapImage = new Image();
const mapImageUploading = ref(false);
const mapConfigUploading = ref(false);
const mapUploadMessage = ref("");
const dockConfigMessage = ref("");
const mapImageInputRef = ref<HTMLInputElement | null>(null);
const mapConfigInputRef = ref<HTMLInputElement | null>(null);
rosMapImage.onload = () => {
@@ -1126,6 +1161,20 @@ function buildDevicePlans(device: DeviceRecord): PatrolTask[] {
];
}
// 回充配置默认值与 charging_dock 节点保持一致:默认开启自动回充,阈值 30%
function buildDockConfig(device: DeviceRecord): DockConfigState {
return {
dockX: device.dockX,
dockY: device.dockY,
dockYaw: device.dockYaw,
preDockX: device.preDockX,
preDockY: device.preDockY,
preDockYaw: device.preDockYaw,
autoRechargeEnabled: device.autoRechargeEnabled ?? true,
lowBatteryThreshold: device.lowBatteryThreshold ?? 30,
};
}
function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
const maps = buildDeviceMaps(device);
const points = buildDevicePoints();
@@ -1142,6 +1191,7 @@ function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
points,
selectedPointIds: [],
plans,
dock: buildDockConfig(device),
};
}
@@ -1173,6 +1223,17 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceRecord {
infraredUrl: car.cameraWebrtcUrl2 || "",
mapPgmUrl: car.mapPgmUrl || "",
mapYamlUrl: car.mapYamlUrl || "",
poseX: car.poseX,
poseY: car.poseY,
poseYaw: car.poseYaw,
dockX: car.dockX,
dockY: car.dockY,
dockYaw: car.dockYaw,
preDockX: car.preDockX,
preDockY: car.preDockY,
preDockYaw: car.preDockYaw,
autoRechargeEnabled: car.autoRechargeEnabled,
lowBatteryThreshold: car.lowBatteryThreshold,
createdAt: car.createTime || "--",
};
}
@@ -1841,6 +1902,23 @@ function renderMapCanvas() {
context.restore();
});
drawDockMarker(
context,
rect,
currentConfig.value?.dock.dockX,
currentConfig.value?.dock.dockY,
"#7dffb0",
"充",
);
drawDockMarker(
context,
rect,
currentConfig.value?.dock.preDockX,
currentConfig.value?.dock.preDockY,
"#8fb4ff",
"预",
);
if (selectionBox.active) {
const left = mapFrame.left + Math.min(selectionBox.startX, selectionBox.endX) * unitX;
const top = mapFrame.top + Math.min(selectionBox.startY, selectionBox.endY) * unitY;
@@ -2359,6 +2437,144 @@ function buildNextPlanId() {
return `${currentConfiguredDevice.value?.id ?? "ROBOT"}-TASK${String(maxIndex + 1).padStart(2, "0")}`;
}
// 巡检点使用 0-100 百分比坐标,充电点使用 ROS 米制坐标,这里做双向换算
function percentToRosPosition(xPercent: number, yPercent: number) {
const resolution = rosMapMeta.resolution || 0.05;
const width = rosMapMeta.width || 0;
const height = rosMapMeta.height || 0;
return {
x: (xPercent / 100) * width * resolution + rosMapMeta.origin[0],
y: ((100 - yPercent) / 100) * height * resolution + rosMapMeta.origin[1],
};
}
function rosToPercentPosition(xRos: number, yRos: number) {
const resolution = rosMapMeta.resolution || 0.05;
const width = rosMapMeta.width || 1;
const height = rosMapMeta.height || 1;
return {
x: ((xRos - rosMapMeta.origin[0]) / resolution / width) * 100,
y: 100 - ((yRos - rosMapMeta.origin[1]) / resolution / height) * 100,
};
}
function roundDockValue(value: number) {
return Number(value.toFixed(4));
}
// 在地图上点选充电点/预对接点,换算成 ROS 坐标后写回回充配置
function applyMapPickToDockPose(mode: "dock" | "preDock", position: { x: number; y: number }) {
if (!currentConfig.value) {
return;
}
const rosPosition = percentToRosPosition(position.x, position.y);
const x = roundDockValue(rosPosition.x);
const y = roundDockValue(rosPosition.y);
if (mode === "dock") {
currentConfig.value.dock.dockX = x;
currentConfig.value.dock.dockY = y;
dockConfigMessage.value = `充电点已更新为 x=${x}, y=${y},朝向可手填或使用当前位姿`;
} else {
currentConfig.value.dock.preDockX = x;
currentConfig.value.dock.preDockY = y;
dockConfigMessage.value = `预对接点已更新为 x=${x}, y=${y},朝向可手填或使用当前位姿`;
}
scheduleMapCanvasRender();
}
// 用小车最近上报的当前位姿填充充电点/预对接点
function fillDockPoseFromRobot(mode: "dock" | "preDock") {
if (!currentConfig.value) {
return;
}
const device = currentConfiguredDevice.value;
if (
typeof device?.poseX !== "number" ||
typeof device?.poseY !== "number" ||
typeof device?.poseYaw !== "number"
) {
dockConfigMessage.value = "未获取到小车当前位姿,无法使用当前位姿填充";
return;
}
const x = roundDockValue(device.poseX);
const y = roundDockValue(device.poseY);
const yaw = roundDockValue(device.poseYaw);
if (mode === "dock") {
currentConfig.value.dock.dockX = x;
currentConfig.value.dock.dockY = y;
currentConfig.value.dock.dockYaw = yaw;
dockConfigMessage.value = `充电点已填入小车当前位姿 x=${x}, y=${y}, yaw=${yaw}`;
} else {
currentConfig.value.dock.preDockX = x;
currentConfig.value.dock.preDockY = y;
currentConfig.value.dock.preDockYaw = yaw;
dockConfigMessage.value = `预对接点已填入小车当前位姿 x=${x}, y=${y}, yaw=${yaw}`;
}
scheduleMapCanvasRender();
}
// 保存充电点与自动回充配置;后端保存后会同步下发给在线小车
async function saveDockConfig() {
const device = currentConfiguredDevice.value;
const config = currentConfig.value;
if (!device || !config) {
return;
}
await updateRosCarDockConfig({
id: device.id,
...config.dock,
});
}
// 在地图上绘制充电点/预对接点标记
function drawDockMarker(
context: CanvasRenderingContext2D,
rect: DOMRect,
xRos: number | undefined,
yRos: number | undefined,
color: string,
label: string,
) {
if (typeof xRos !== "number" || typeof yRos !== "number") {
return;
}
const percentPosition = rosToPercentPosition(xRos, yRos);
if (!Number.isFinite(percentPosition.x) || !Number.isFinite(percentPosition.y)) {
return;
}
const pixelPoint = percentToStagePixels(percentPosition.x, percentPosition.y, rect);
context.save();
context.beginPath();
context.arc(pixelPoint.x, pixelPoint.y, 11, 0, Math.PI * 2);
context.fillStyle = "rgba(5, 18, 38, 0.94)";
context.fill();
context.lineWidth = 2;
context.strokeStyle = color;
context.shadowColor = color;
context.shadowBlur = 12;
context.stroke();
context.shadowBlur = 0;
context.fillStyle = color;
context.font = '700 10px "Microsoft YaHei", sans-serif';
context.textAlign = "center";
context.fillText(label, pixelPoint.x, pixelPoint.y + 0.5);
context.restore();
}
function handleMapStageClick(event: MouseEvent) {
if (suppressStageClick.value) {
window.setTimeout(() => {
@@ -2376,6 +2592,11 @@ function handleMapStageClick(event: MouseEvent) {
return;
}
if (mapEditorMode.value === "dock" || mapEditorMode.value === "preDock") {
applyMapPickToDockPose(mapEditorMode.value, position);
return;
}
const hitPoint = findHitPoint(event);
if (hitPoint) {
handlePointClick(hitPoint.id, event);
@@ -2562,6 +2783,7 @@ async function saveConfigDialog() {
try {
await saveCurrentMapUrls();
await saveDockConfig();
await saveRosInspectionTask(
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
);
@@ -3117,6 +3339,86 @@ watch(
</div>
</div>
<div class="config-side-card dock-config-board">
<div class="dock-config-head">
<span class="dock-config-title">充电回充配置</span>
<div class="dock-switch-row">
<label class="dock-switch">
<input v-model="currentConfig.dock.autoRechargeEnabled" type="checkbox" />
<span>自动回充</span>
</label>
<label class="dock-threshold">
<span>阈值</span>
<input v-model.number="currentConfig.dock.lowBatteryThreshold" max="99" min="1" step="1"
type="number" />
<span>%</span>
</label>
</div>
</div>
<div class="dock-pose-block">
<div class="dock-pose-head">
<span class="dock-pose-title">充电点</span>
<div class="dock-pose-actions">
<button :class="{ primary: mapEditorMode === 'dock' }" class="page-action-btn ghost" type="button"
@click="mapEditorMode = 'dock'">
地图点选
</button>
<button class="page-action-btn ghost" type="button" @click="fillDockPoseFromRobot('dock')">
当前位姿
</button>
</div>
</div>
<div class="dock-pose-grid">
<label class="config-field">
<span>X</span>
<input v-model.number="currentConfig.dock.dockX" step="0.001" type="number" />
</label>
<label class="config-field">
<span>Y</span>
<input v-model.number="currentConfig.dock.dockY" step="0.001" type="number" />
</label>
<label class="config-field">
<span>Yaw</span>
<input v-model.number="currentConfig.dock.dockYaw" step="0.001" type="number" />
</label>
</div>
</div>
<div class="dock-pose-block">
<div class="dock-pose-head">
<span class="dock-pose-title">预对接点</span>
<div class="dock-pose-actions">
<button :class="{ primary: mapEditorMode === 'preDock' }" class="page-action-btn ghost" type="button"
@click="mapEditorMode = 'preDock'">
地图点选
</button>
<button class="page-action-btn ghost" type="button" @click="fillDockPoseFromRobot('preDock')">
当前位姿
</button>
</div>
</div>
<div class="dock-pose-grid">
<label class="config-field">
<span>X</span>
<input v-model.number="currentConfig.dock.preDockX" step="0.001" type="number" />
</label>
<label class="config-field">
<span>Y</span>
<input v-model.number="currentConfig.dock.preDockY" step="0.001" type="number" />
</label>
<label class="config-field">
<span>Yaw</span>
<input v-model.number="currentConfig.dock.preDockYaw" step="0.001" type="number" />
</label>
</div>
</div>
<div v-if="dockConfigMessage" class="dock-config-message">
{{ dockConfigMessage }}
</div>
</div>
</section>
</div>
@@ -3347,7 +3649,7 @@ watch(
}
.redesigned-side-panel {
gap: 16px;
gap: 10px;
}
.panel-head.modern {
@@ -3387,6 +3689,123 @@ watch(
padding: 16px;
}
.dock-config-board {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
}
.dock-config-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.dock-config-title {
color: #f0fbff;
font-size: 13px;
font-weight: 700;
}
.dock-switch-row {
display: flex;
align-items: center;
gap: 12px;
}
.dock-switch {
display: flex;
align-items: center;
gap: 5px;
color: rgba(200, 240, 255, 0.86);
font-size: 12px;
cursor: pointer;
}
.dock-switch input {
width: 14px;
height: 14px;
margin: 0;
accent-color: #4fd1ff;
}
.dock-threshold {
display: flex;
align-items: center;
gap: 4px;
color: rgba(173, 224, 246, 0.78);
font-size: 12px;
}
.dock-threshold input {
width: 54px;
padding: 3px 6px;
border: 1px solid rgba(85, 198, 255, 0.18);
background: rgba(8, 31, 62, 0.72);
color: #eefcff;
font-size: 12px;
box-sizing: border-box;
outline: none;
}
.dock-pose-block {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border: 1px solid rgba(85, 198, 255, 0.14);
background: rgba(7, 26, 50, 0.6);
}
.dock-pose-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.dock-pose-title {
color: rgba(210, 244, 255, 0.92);
font-size: 12px;
font-weight: 700;
}
.dock-pose-actions {
display: flex;
gap: 6px;
}
.dock-pose-actions .page-action-btn {
padding: 2px 8px;
font-size: 11px;
line-height: 1.6;
}
.dock-pose-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.dock-pose-grid .config-field {
gap: 3px;
font-size: 11px;
}
.dock-pose-grid .config-field input {
padding: 3px 6px;
font-size: 12px;
}
.dock-config-message {
color: rgba(150, 240, 200, 0.86);
font-size: 11px;
line-height: 1.5;
}
.panel-head {
display: flex;
align-items: flex-start;
@@ -4030,8 +4449,9 @@ watch(
.rounds-board {
padding: 16px;
flex: 1;
min-height: 0;
flex: 1 1 auto;
/* 新增充电回充卡片后,轮次配置不再设为 0 基准高度,避免被压缩到看不见 */
min-height: 300px;
display: flex;
flex-direction: column;
}

View File

@@ -11,7 +11,10 @@ import {
getCurrentRosInspection,
getRosCarList,
getRosInspectionTaskDetail,
startRosCharging,
stopDispatchRosInspectionTask,
undockRosCharging,
type RosChargingResult,
} from "@/api/rosCar";
import { getToken } from "@/utils/auth";
import { parseRosMapYaml } from "@/utils/rosMapYaml";
@@ -42,6 +45,8 @@ interface RosCarRecord {
cameraWebrtcUrl2?: string;
liftHeight?: number;
liftDeviceStatusDescription?: string;
dockStatus?: string;
dockActive?: boolean;
}
interface RosCarListResult {
@@ -69,6 +74,8 @@ interface CarStatusPushData {
mapYamlUrl?: string;
status?: number;
signal?: string;
dockStatus?: string;
dockActive?: boolean;
}
interface CarVelocityPushData {
@@ -115,6 +122,8 @@ interface DeviceItem {
mapYamlUrl: string;
whiteLightUrl: string;
infraredUrl: string;
dockStatus?: string;
dockActive?: boolean;
}
interface ProjectItem {
@@ -195,6 +204,11 @@ interface RosDispatchResultResponse {
msg?: string;
}
interface RosChargingResultResponse {
data?: RosChargingResult;
msg?: string;
}
interface AlertItem {
id: string;
title: string;
@@ -286,6 +300,8 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceItem {
mapYamlUrl: car.mapYamlUrl || "",
whiteLightUrl: car.cameraWebrtcUrl || "",
infraredUrl: car.cameraWebrtcUrl2 || "",
dockStatus: car.dockStatus,
dockActive: car.dockActive ?? false,
};
}
@@ -494,6 +510,8 @@ const inspectionTaskId = ref("");
const currentInspection = ref<RosInspectionRecord | null>(null);
const inspectionLoading = ref(false);
const inspectionActionLoading = ref(false);
const chargingActionLoading = ref(false);
const undockingActionLoading = ref(false);
const lastDispatchTime = ref("");
const alertLoading = ref(false);
const alertError = ref("");
@@ -614,6 +632,27 @@ const currentScenario = computed(() => navScenario);
const hasRobotPose = computed(
() => typeof currentRobot.value.poseX === "number" && typeof currentRobot.value.poseY === "number",
);
// 回充状态文案:由 charging_dock 节点的 /dock_status 上报,未上报时给出中性提示
const chargingStatusText = computed(() => {
const device = currentRobot.value;
if (device.dockActive) {
return device.dockStatus || "自动回充进行中";
}
return device.dockStatus || "回充状态未上报";
});
const chargingButtonText = computed(() => {
if (currentRobot.value.dockActive) {
return "回充中...";
}
return chargingActionLoading.value ? "处理中..." : "自动回充";
});
const undockingButtonText = computed(() => {
if (currentRobot.value.dockActive) {
return "处理中...";
}
return undockingActionLoading.value ? "处理中..." : "充电脱离";
});
// 将 ROS 米制坐标转换成地图图片像素坐标,路线和机器人定位共用同一套转换
function rosPoseToMapPoint(poseX: number, poseY: number, poseYaw = 0): RobotMapPoint | null {
@@ -861,6 +900,74 @@ function confirmStopInspection() {
showConfirmDialog("停止巡检", "确认停止当前巡检任务吗?", () => runInspectionAction("stop"));
}
// 手动触发一次自动回充;需要小车在线且车端已启动回充节点
async function runStartCharging() {
const carId = currentRobot.value.carId;
if (!carId) {
window.$message.warning("未获取到小车ID,请先在设备管理中确认小车已保存");
return;
}
chargingActionLoading.value = true;
try {
const response = (await startRosCharging(carId)) as RosChargingResultResponse;
const accepted = response.data?.accepted ?? false;
const message = response.data?.message
|| response.msg
|| (accepted ? "已触发自动回充" : "触发自动回充失败");
if (accepted) {
window.$message.success(message);
} else {
window.$message.warning(message);
}
} catch (error) {
console.error(error);
} finally {
chargingActionLoading.value = false;
}
}
function confirmStartCharging() {
showConfirmDialog("自动回充", "确认让小车执行自动回充吗?", () => runStartCharging());
}
// 手动触发一次脱离充电桩;用于充电完成后手动让小车离开充电桩
async function runUndockCharging() {
const carId = currentRobot.value.carId;
if (!carId) {
window.$message.warning("未获取到小车ID,请先在设备管理中确认小车已保存");
return;
}
undockingActionLoading.value = true;
try {
const response = (await undockRosCharging(carId)) as RosChargingResultResponse;
const accepted = response.data?.accepted ?? false;
const message = response.data?.message
|| response.msg
|| (accepted ? "已触发脱离充电桩" : "触发脱离充电桩失败");
if (accepted) {
window.$message.success(message);
} else {
window.$message.warning(message);
}
} catch (error) {
console.error(error);
} finally {
undockingActionLoading.value = false;
}
}
function confirmUndockCharging() {
showConfirmDialog("充电脱离", "确认让小车脱离充电桩吗?", () => runUndockCharging());
}
// 切换设备时将当前设备已有升降柱高度回显到目标高度输入框
function syncLiftTargetFromDevice(device?: DeviceItem) {
if (!device || device.liftHeight === "--") {
@@ -921,6 +1028,8 @@ function applyCarStatusPush(data: CarStatusPushData) {
poseYaw: typeof data.poseYaw === "number" ? data.poseYaw : currentDevice.poseYaw,
mapPgmUrl: data.mapPgmUrl || currentDevice.mapPgmUrl,
mapYamlUrl: data.mapYamlUrl || currentDevice.mapYamlUrl,
dockStatus: typeof data.dockStatus === "string" ? data.dockStatus : currentDevice.dockStatus,
dockActive: typeof data.dockActive === "boolean" ? data.dockActive : currentDevice.dockActive,
};
projectDevices.value[0].devices.splice(deviceIndex, 1, nextDevice);
@@ -1868,6 +1977,26 @@ function getStatusClass(status: DeviceItem["status"]) {
>
{{ inspectionActionLoading ? "处理中..." : "下发巡检" }}
</button>
<div class="charging-status-row">
<span :class="['charging-status-dot', { active: currentRobot.dockActive }]"></span>
<span class="charging-status-text">{{ chargingStatusText }}</span>
</div>
<button
class="inspection-action-btn charging"
type="button"
:disabled="chargingActionLoading || currentRobot.dockActive"
@click="confirmStartCharging"
>
{{ chargingButtonText }}
</button>
<button
class="inspection-action-btn undock"
type="button"
:disabled="undockingActionLoading || currentRobot.dockActive"
@click="confirmUndockCharging"
>
{{ undockingButtonText }}
</button>
</div>
</div>
</div>

View File

@@ -25,7 +25,9 @@
"poseY": 6.789,
"poseYaw": 1.57,
"status": 1,
"signal": "-58dBm"
"signal": "-58dBm",
"dockStatus": "自动回充待机中",
"dockActive": false
}
}
```
@@ -42,6 +44,8 @@
| `data.poseYaw` | 否 | 当前朝向 |
| `data.status` | 否 | 小车状态:`0` 空闲,`1` 巡检,`2` 离线,`3` 充电中 |
| `data.signal` | 否 | 小车信号 |
| `data.dockStatus` | 否 | 自动回充状态文本,取自 `/dock_status` |
| `data.dockActive` | 否 | 回充或脱离充电桩是否进行中,取自 `/auto_docking_active` 与 `/auto_undocking_active` |
说明:
@@ -77,7 +81,9 @@
"poseY": 6.789,
"poseYaw": 1.57,
"status": 1,
"signal": "-58dBm"
"signal": "-58dBm",
"dockStatus": "自动回充待机中",
"dockActive": false
},
"timestamp": 1782702000010
}
@@ -85,12 +91,14 @@
顶层字段说明:
| 字段 | 说明 |
| ---------------- | ---------------------------------------- |
| `type` | 固定为 `car_status` |
| `data.deviceKey` | 小车设备标识 |
| `data.liftHeight` | 升降柱高度,单位 mm |
| `timestamp` | 后端推送时间戳,毫秒 |
| 字段 | 说明 |
| ------------------ | ------------------------------------------------------- |
| `type` | 固定为 `car_status` |
| `data.deviceKey` | 小车设备标识 |
| `data.liftHeight` | 升降柱高度,单位 mm |
| `data.dockStatus` | 自动回充状态文本;用于前端回显当前回充阶段 |
| `data.dockActive` | 回充或脱离充电桩是否进行中;为 `true` 时前端禁用「自动回充」「充电脱离」按钮 |
| `timestamp` | 后端推送时间戳,毫秒 |
## 小车速度上报消息

View File

@@ -165,6 +165,74 @@ uint16 height
| `3` | 停止 | `0` |
| `4` | 运行到指定高度 | 目标高度,单位 mm |
## 充电点与回充配置
ROS 包参考:`charging_dock_interfaces/srv/SetDockPose`、`charging_dock_interfaces/srv/SetDockConfig`
消息类型:`dock_config`
```json
{
"type": "dock_config",
"requestId": "dock-1",
"data": {
"deviceKey": "ROBOT-001",
"dockX": 1.235,
"dockY": -0.42,
"dockYaw": 0.0,
"preDockX": 1.8,
"preDockY": -0.42,
"preDockYaw": 0.0,
"autoRechargeEnabled": true,
"lowBatteryThreshold": 30
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `dockX` / `dockY` / `dockYaw` | 充电点(对接点)位姿,`map` 坐标系,单位米 / 弧度 |
| `preDockX` / `preDockY` / `preDockYaw` | 预对接点位姿,同上 |
| `autoRechargeEnabled` | 是否开启低电量自动回充 |
| `lowBatteryThreshold` | 自动回充触发的电量阈值(%) |
说明:
- 三组字段互相独立:ROS 端只对**同时提供完整三元组**的点位调用 `/charging_dock/set_dock_pose` 或 `/charging_dock/set_pre_dock_pose`,缺任一字段则跳过;`autoRechargeEnabled` 与 `lowBatteryThreshold` 任一缺失时跳过 `/charging_dock/set_config`。
- 该消息由后端 `POST /admin/rosCar/updateDockConfig` 保存成功后下发,浏览器端不直接发送。
- 小车离线时后端只落库、不下发,需要重新保存一次才能同步到车端。
- 服务不可用时 ROS 端只记录告警,不回执失败。
## 回充与脱离充电桩
消息类型:`dock_start`(触发自动回充)、`dock_undock`(脱离充电桩)
```json
{
"type": "dock_undock",
"requestId": "dock-undock-1",
"data": {
"deviceKey": "ROBOT-001"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `deviceKey` | 控制目标机器人设备标识 |
说明:
- 两条消息都由后端接口下发,浏览器端不直接发送:回充 `POST /admin/rosCar/startCharging`,脱离 `POST /admin/rosCar/undockCharging`,参数都是 `carId`。
- 车端分别调用 `/charging_dock/start` 与 `/charging_dock/undock`(均为 `std_srvs/Trigger`)。
- `accepted=true` 只代表车端已受理,机械动作在车端异步执行;执行期间 `dockActive` 为 `true`,进度由 `dockStatus` 文本回显(`/auto_docking_active`、`/auto_undocking_active` 合并上报)。
- 脱离动作:车端切到横移模式后向左侧退出约 `undock_distance`(默认 0.30 m)离开充电面板,完成后恢复阿克曼模式。
- 充电完成自动脱离:车端电量升到 `undock_battery_threshold`(默认 95%),且本次到桩时电量低于(阈值 - `undock_battery_start_margin`,默认 3%)时,自动触发一次脱离。
## 机器人端收到的消息
设备端连接:

View File

@@ -36,7 +36,7 @@ export default defineConfig(({ command }: ConfigEnv) => {
rewrite: (path: string) => path.replace(/^\/api/, '')
},
'/ws': {
target: `ws://192.168.3.110:16003/`,
target: `ws://127.0.0.1:16003/`,
changeOrigin: true,
ws: true
}