From fe56a8ba6d5ee81bf31996b96626c8e979f25661 Mon Sep 17 00:00:00 2001 From: huaqiang Date: Fri, 18 Sep 2026 16:51:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=85=E7=94=B5=E5=92=8C=E8=84=B1=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/rosCar.ts | 78 ++++ src/views/dashboard1/index.less | 52 +++ .../pages/EquipmentManagementPage.vue | 428 +++++++++++++++++- .../dashboard1/pages/IntegratedCenterPage.vue | 129 ++++++ .../dashboard1/pages/小车状态WS推送协议.md | 24 +- .../dashboard1/pages/浏览器WS控制协议.md | 68 +++ vite.config.ts | 2 +- 7 files changed, 768 insertions(+), 13 deletions(-) diff --git a/src/api/rosCar.ts b/src/api/rosCar.ts index 803898d..91848c0 100644 --- a/src/api/rosCar.ts +++ b/src/api/rosCar.ts @@ -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>({ + url: "/admin/rosCar/updateDockConfig", + data, + }); +} + +/** + * 手动触发回充结果,对应后端 com.haizhiyustc.ros.dto.RosChargingResultDTO + */ +export interface RosChargingResult { + /** 小车是否接受本次触发 */ + accepted?: boolean; + /** 触发结果说明 */ + message?: string; +} + +// 手动触发小车自动回充 +export function startRosCharging(carId: string) { + return request>({ + url: "/admin/rosCar/startCharging", + params: { carId }, + }); +} + +// 手动触发小车脱离充电桩 +export function undockRosCharging(carId: string) { + return request>({ + url: "/admin/rosCar/undockCharging", + params: { carId }, + }); +} diff --git a/src/views/dashboard1/index.less b/src/views/dashboard1/index.less index 008f5d3..eeac75a 100644 --- a/src/views/dashboard1/index.less +++ b/src/views/dashboard1/index.less @@ -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); diff --git a/src/views/dashboard1/pages/EquipmentManagementPage.vue b/src/views/dashboard1/pages/EquipmentManagementPage.vue index 7383d48..3aabe85 100644 --- a/src/views/dashboard1/pages/EquipmentManagementPage.vue +++ b/src/views/dashboard1/pages/EquipmentManagementPage.vue @@ -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([]); @@ -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(null); const mapConfigInputRef = ref(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( +
+
+ 充电回充配置 +
+ + +
+
+ +
+
+ 充电点 +
+ + +
+
+
+ + + +
+
+ +
+
+ 预对接点 +
+ + +
+
+
+ + + +
+
+ +
+ {{ dockConfigMessage }} +
+
+ @@ -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; } diff --git a/src/views/dashboard1/pages/IntegratedCenterPage.vue b/src/views/dashboard1/pages/IntegratedCenterPage.vue index 9b53f73..97ae936 100644 --- a/src/views/dashboard1/pages/IntegratedCenterPage.vue +++ b/src/views/dashboard1/pages/IntegratedCenterPage.vue @@ -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(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 ? "处理中..." : "下发巡检" }} +
+ + {{ chargingStatusText }} +
+ + diff --git a/src/views/dashboard1/pages/小车状态WS推送协议.md b/src/views/dashboard1/pages/小车状态WS推送协议.md index 8d9b500..8933aba 100644 --- a/src/views/dashboard1/pages/小车状态WS推送协议.md +++ b/src/views/dashboard1/pages/小车状态WS推送协议.md @@ -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` | 后端推送时间戳,毫秒 | ## 小车速度上报消息 diff --git a/src/views/dashboard1/pages/浏览器WS控制协议.md b/src/views/dashboard1/pages/浏览器WS控制协议.md index 2cea9c5..4a04920 100644 --- a/src/views/dashboard1/pages/浏览器WS控制协议.md +++ b/src/views/dashboard1/pages/浏览器WS控制协议.md @@ -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%)时,自动触发一次脱离。 + ## 机器人端收到的消息 设备端连接: diff --git a/vite.config.ts b/vite.config.ts index e7f3b1f..9c270f8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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 }