充电和脱离
This commit is contained in:
@@ -70,6 +70,26 @@ export interface RosCar {
|
|||||||
liftHeight?: number;
|
liftHeight?: number;
|
||||||
/** 升降柱状态 */
|
/** 升降柱状态 */
|
||||||
lift?: RosLift;
|
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;
|
createTime?: string;
|
||||||
/** 更新时间 */
|
/** 更新时间 */
|
||||||
@@ -293,3 +313,61 @@ export function updateRosCarMapUrls(data: {
|
|||||||
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -2655,6 +2655,58 @@
|
|||||||
color: #ffd9d5;
|
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) {
|
.inspection-action-btn:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
border-color: rgba(102, 235, 255, 0.72);
|
border-color: rgba(102, 235, 255, 0.72);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getRosCarList,
|
getRosCarList,
|
||||||
getRosInspectionTaskDetail,
|
getRosInspectionTaskDetail,
|
||||||
saveRosInspectionTask,
|
saveRosInspectionTask,
|
||||||
|
updateRosCarDockConfig,
|
||||||
updateRosCarMapUrls,
|
updateRosCarMapUrls,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
} from "@/api/rosCar";
|
} from "@/api/rosCar";
|
||||||
@@ -19,7 +20,7 @@ type RecognitionActionCode =
|
|||||||
| "TUNNEL_WATER"
|
| "TUNNEL_WATER"
|
||||||
| "WALL_CRACK";
|
| "WALL_CRACK";
|
||||||
type PatrolTaskMode = "定时" | "周期" | "临时";
|
type PatrolTaskMode = "定时" | "周期" | "临时";
|
||||||
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint";
|
type EditorMode = "select" | "normal" | "thermal" | "pump" | "waypoint" | "dock" | "preDock";
|
||||||
|
|
||||||
interface RosCarRecord {
|
interface RosCarRecord {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -42,6 +43,14 @@ interface RosCarRecord {
|
|||||||
liftHeight?: number;
|
liftHeight?: number;
|
||||||
mapPgmUrl?: string;
|
mapPgmUrl?: string;
|
||||||
mapYamlUrl?: string;
|
mapYamlUrl?: string;
|
||||||
|
dockX?: number;
|
||||||
|
dockY?: number;
|
||||||
|
dockYaw?: number;
|
||||||
|
preDockX?: number;
|
||||||
|
preDockY?: number;
|
||||||
|
preDockYaw?: number;
|
||||||
|
autoRechargeEnabled?: boolean;
|
||||||
|
lowBatteryThreshold?: number;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
}
|
}
|
||||||
@@ -67,6 +76,17 @@ interface DeviceRecord {
|
|||||||
infraredUrl?: string;
|
infraredUrl?: string;
|
||||||
mapPgmUrl?: string;
|
mapPgmUrl?: string;
|
||||||
mapYamlUrl?: 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;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +210,17 @@ interface PatrolTask {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DockConfigState {
|
||||||
|
dockX?: number;
|
||||||
|
dockY?: number;
|
||||||
|
dockYaw?: number;
|
||||||
|
preDockX?: number;
|
||||||
|
preDockY?: number;
|
||||||
|
preDockYaw?: number;
|
||||||
|
autoRechargeEnabled: boolean;
|
||||||
|
lowBatteryThreshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface DeviceConfigState {
|
interface DeviceConfigState {
|
||||||
maps: MapRecord[];
|
maps: MapRecord[];
|
||||||
activeMapId: string;
|
activeMapId: string;
|
||||||
@@ -201,6 +232,7 @@ interface DeviceConfigState {
|
|||||||
points: PatrolPoint[];
|
points: PatrolPoint[];
|
||||||
selectedPointIds: string[];
|
selectedPointIds: string[];
|
||||||
plans: PatrolTask[];
|
plans: PatrolTask[];
|
||||||
|
dock: DockConfigState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlanFormState {
|
interface PlanFormState {
|
||||||
@@ -243,6 +275,8 @@ let patrolActionIdSeed = 0;
|
|||||||
const editorModes: Array<{ id: EditorMode; label: string }> = [
|
const editorModes: Array<{ id: EditorMode; label: string }> = [
|
||||||
{ id: "normal", label: "新增业务点" },
|
{ id: "normal", label: "新增业务点" },
|
||||||
{ id: "waypoint", label: "新增途经点" },
|
{ id: "waypoint", label: "新增途经点" },
|
||||||
|
{ id: "dock", label: "设置充电点" },
|
||||||
|
{ id: "preDock", label: "设置预对接点" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const devices = ref<DeviceRecord[]>([]);
|
const devices = ref<DeviceRecord[]>([]);
|
||||||
@@ -371,6 +405,7 @@ const rosMapImage = new Image();
|
|||||||
const mapImageUploading = ref(false);
|
const mapImageUploading = ref(false);
|
||||||
const mapConfigUploading = ref(false);
|
const mapConfigUploading = ref(false);
|
||||||
const mapUploadMessage = ref("");
|
const mapUploadMessage = ref("");
|
||||||
|
const dockConfigMessage = ref("");
|
||||||
const mapImageInputRef = ref<HTMLInputElement | null>(null);
|
const mapImageInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const mapConfigInputRef = ref<HTMLInputElement | null>(null);
|
const mapConfigInputRef = ref<HTMLInputElement | null>(null);
|
||||||
rosMapImage.onload = () => {
|
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 {
|
function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
|
||||||
const maps = buildDeviceMaps(device);
|
const maps = buildDeviceMaps(device);
|
||||||
const points = buildDevicePoints();
|
const points = buildDevicePoints();
|
||||||
@@ -1142,6 +1191,7 @@ function buildDeviceConfig(device: DeviceRecord): DeviceConfigState {
|
|||||||
points,
|
points,
|
||||||
selectedPointIds: [],
|
selectedPointIds: [],
|
||||||
plans,
|
plans,
|
||||||
|
dock: buildDockConfig(device),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1173,6 +1223,17 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceRecord {
|
|||||||
infraredUrl: car.cameraWebrtcUrl2 || "",
|
infraredUrl: car.cameraWebrtcUrl2 || "",
|
||||||
mapPgmUrl: car.mapPgmUrl || "",
|
mapPgmUrl: car.mapPgmUrl || "",
|
||||||
mapYamlUrl: car.mapYamlUrl || "",
|
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 || "--",
|
createdAt: car.createTime || "--",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1841,6 +1902,23 @@ function renderMapCanvas() {
|
|||||||
context.restore();
|
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) {
|
if (selectionBox.active) {
|
||||||
const left = mapFrame.left + Math.min(selectionBox.startX, selectionBox.endX) * unitX;
|
const left = mapFrame.left + Math.min(selectionBox.startX, selectionBox.endX) * unitX;
|
||||||
const top = mapFrame.top + Math.min(selectionBox.startY, selectionBox.endY) * unitY;
|
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")}`;
|
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) {
|
function handleMapStageClick(event: MouseEvent) {
|
||||||
if (suppressStageClick.value) {
|
if (suppressStageClick.value) {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -2376,6 +2592,11 @@ function handleMapStageClick(event: MouseEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mapEditorMode.value === "dock" || mapEditorMode.value === "preDock") {
|
||||||
|
applyMapPickToDockPose(mapEditorMode.value, position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hitPoint = findHitPoint(event);
|
const hitPoint = findHitPoint(event);
|
||||||
if (hitPoint) {
|
if (hitPoint) {
|
||||||
handlePointClick(hitPoint.id, event);
|
handlePointClick(hitPoint.id, event);
|
||||||
@@ -2562,6 +2783,7 @@ async function saveConfigDialog() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await saveCurrentMapUrls();
|
await saveCurrentMapUrls();
|
||||||
|
await saveDockConfig();
|
||||||
await saveRosInspectionTask(
|
await saveRosInspectionTask(
|
||||||
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
|
buildInspectionTaskPayload(currentConfiguredDevice.value, currentConfig.value),
|
||||||
);
|
);
|
||||||
@@ -3117,6 +3339,86 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3347,7 +3649,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.redesigned-side-panel {
|
.redesigned-side-panel {
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-head.modern {
|
.panel-head.modern {
|
||||||
@@ -3387,6 +3689,123 @@ watch(
|
|||||||
padding: 16px;
|
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 {
|
.panel-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -4030,8 +4449,9 @@ watch(
|
|||||||
|
|
||||||
.rounds-board {
|
.rounds-board {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
/* 新增充电回充卡片后,轮次配置不再设为 0 基准高度,避免被压缩到看不见 */
|
||||||
|
min-height: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
getCurrentRosInspection,
|
getCurrentRosInspection,
|
||||||
getRosCarList,
|
getRosCarList,
|
||||||
getRosInspectionTaskDetail,
|
getRosInspectionTaskDetail,
|
||||||
|
startRosCharging,
|
||||||
stopDispatchRosInspectionTask,
|
stopDispatchRosInspectionTask,
|
||||||
|
undockRosCharging,
|
||||||
|
type RosChargingResult,
|
||||||
} from "@/api/rosCar";
|
} from "@/api/rosCar";
|
||||||
import { getToken } from "@/utils/auth";
|
import { getToken } from "@/utils/auth";
|
||||||
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||||
@@ -42,6 +45,8 @@ interface RosCarRecord {
|
|||||||
cameraWebrtcUrl2?: string;
|
cameraWebrtcUrl2?: string;
|
||||||
liftHeight?: number;
|
liftHeight?: number;
|
||||||
liftDeviceStatusDescription?: string;
|
liftDeviceStatusDescription?: string;
|
||||||
|
dockStatus?: string;
|
||||||
|
dockActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RosCarListResult {
|
interface RosCarListResult {
|
||||||
@@ -69,6 +74,8 @@ interface CarStatusPushData {
|
|||||||
mapYamlUrl?: string;
|
mapYamlUrl?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
signal?: string;
|
signal?: string;
|
||||||
|
dockStatus?: string;
|
||||||
|
dockActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CarVelocityPushData {
|
interface CarVelocityPushData {
|
||||||
@@ -115,6 +122,8 @@ interface DeviceItem {
|
|||||||
mapYamlUrl: string;
|
mapYamlUrl: string;
|
||||||
whiteLightUrl: string;
|
whiteLightUrl: string;
|
||||||
infraredUrl: string;
|
infraredUrl: string;
|
||||||
|
dockStatus?: string;
|
||||||
|
dockActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectItem {
|
interface ProjectItem {
|
||||||
@@ -195,6 +204,11 @@ interface RosDispatchResultResponse {
|
|||||||
msg?: string;
|
msg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RosChargingResultResponse {
|
||||||
|
data?: RosChargingResult;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface AlertItem {
|
interface AlertItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -286,6 +300,8 @@ function normalizeDeviceRecord(car: RosCarRecord, index: number): DeviceItem {
|
|||||||
mapYamlUrl: car.mapYamlUrl || "",
|
mapYamlUrl: car.mapYamlUrl || "",
|
||||||
whiteLightUrl: car.cameraWebrtcUrl || "",
|
whiteLightUrl: car.cameraWebrtcUrl || "",
|
||||||
infraredUrl: car.cameraWebrtcUrl2 || "",
|
infraredUrl: car.cameraWebrtcUrl2 || "",
|
||||||
|
dockStatus: car.dockStatus,
|
||||||
|
dockActive: car.dockActive ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +510,8 @@ const inspectionTaskId = ref("");
|
|||||||
const currentInspection = ref<RosInspectionRecord | null>(null);
|
const currentInspection = ref<RosInspectionRecord | null>(null);
|
||||||
const inspectionLoading = ref(false);
|
const inspectionLoading = ref(false);
|
||||||
const inspectionActionLoading = ref(false);
|
const inspectionActionLoading = ref(false);
|
||||||
|
const chargingActionLoading = ref(false);
|
||||||
|
const undockingActionLoading = ref(false);
|
||||||
const lastDispatchTime = ref("");
|
const lastDispatchTime = ref("");
|
||||||
const alertLoading = ref(false);
|
const alertLoading = ref(false);
|
||||||
const alertError = ref("");
|
const alertError = ref("");
|
||||||
@@ -614,6 +632,27 @@ const currentScenario = computed(() => navScenario);
|
|||||||
const hasRobotPose = computed(
|
const hasRobotPose = computed(
|
||||||
() => typeof currentRobot.value.poseX === "number" && typeof currentRobot.value.poseY === "number",
|
() => 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 米制坐标转换成地图图片像素坐标,路线和机器人定位共用同一套转换
|
// 将 ROS 米制坐标转换成地图图片像素坐标,路线和机器人定位共用同一套转换
|
||||||
function rosPoseToMapPoint(poseX: number, poseY: number, poseYaw = 0): RobotMapPoint | null {
|
function rosPoseToMapPoint(poseX: number, poseY: number, poseYaw = 0): RobotMapPoint | null {
|
||||||
@@ -861,6 +900,74 @@ function confirmStopInspection() {
|
|||||||
showConfirmDialog("停止巡检", "确认停止当前巡检任务吗?", () => runInspectionAction("stop"));
|
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) {
|
function syncLiftTargetFromDevice(device?: DeviceItem) {
|
||||||
if (!device || device.liftHeight === "--") {
|
if (!device || device.liftHeight === "--") {
|
||||||
@@ -921,6 +1028,8 @@ function applyCarStatusPush(data: CarStatusPushData) {
|
|||||||
poseYaw: typeof data.poseYaw === "number" ? data.poseYaw : currentDevice.poseYaw,
|
poseYaw: typeof data.poseYaw === "number" ? data.poseYaw : currentDevice.poseYaw,
|
||||||
mapPgmUrl: data.mapPgmUrl || currentDevice.mapPgmUrl,
|
mapPgmUrl: data.mapPgmUrl || currentDevice.mapPgmUrl,
|
||||||
mapYamlUrl: data.mapYamlUrl || currentDevice.mapYamlUrl,
|
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);
|
projectDevices.value[0].devices.splice(deviceIndex, 1, nextDevice);
|
||||||
@@ -1868,6 +1977,26 @@ function getStatusClass(status: DeviceItem["status"]) {
|
|||||||
>
|
>
|
||||||
{{ inspectionActionLoading ? "处理中..." : "下发巡检" }}
|
{{ inspectionActionLoading ? "处理中..." : "下发巡检" }}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
"poseY": 6.789,
|
"poseY": 6.789,
|
||||||
"poseYaw": 1.57,
|
"poseYaw": 1.57,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"signal": "-58dBm"
|
"signal": "-58dBm",
|
||||||
|
"dockStatus": "自动回充待机中",
|
||||||
|
"dockActive": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -42,6 +44,8 @@
|
|||||||
| `data.poseYaw` | 否 | 当前朝向 |
|
| `data.poseYaw` | 否 | 当前朝向 |
|
||||||
| `data.status` | 否 | 小车状态:`0` 空闲,`1` 巡检,`2` 离线,`3` 充电中 |
|
| `data.status` | 否 | 小车状态:`0` 空闲,`1` 巡检,`2` 离线,`3` 充电中 |
|
||||||
| `data.signal` | 否 | 小车信号 |
|
| `data.signal` | 否 | 小车信号 |
|
||||||
|
| `data.dockStatus` | 否 | 自动回充状态文本,取自 `/dock_status` |
|
||||||
|
| `data.dockActive` | 否 | 回充或脱离充电桩是否进行中,取自 `/auto_docking_active` 与 `/auto_undocking_active` |
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@@ -77,7 +81,9 @@
|
|||||||
"poseY": 6.789,
|
"poseY": 6.789,
|
||||||
"poseYaw": 1.57,
|
"poseYaw": 1.57,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"signal": "-58dBm"
|
"signal": "-58dBm",
|
||||||
|
"dockStatus": "自动回充待机中",
|
||||||
|
"dockActive": false
|
||||||
},
|
},
|
||||||
"timestamp": 1782702000010
|
"timestamp": 1782702000010
|
||||||
}
|
}
|
||||||
@@ -85,12 +91,14 @@
|
|||||||
|
|
||||||
顶层字段说明:
|
顶层字段说明:
|
||||||
|
|
||||||
| 字段 | 说明 |
|
| 字段 | 说明 |
|
||||||
| ---------------- | ---------------------------------------- |
|
| ------------------ | ------------------------------------------------------- |
|
||||||
| `type` | 固定为 `car_status` |
|
| `type` | 固定为 `car_status` |
|
||||||
| `data.deviceKey` | 小车设备标识 |
|
| `data.deviceKey` | 小车设备标识 |
|
||||||
| `data.liftHeight` | 升降柱高度,单位 mm |
|
| `data.liftHeight` | 升降柱高度,单位 mm |
|
||||||
| `timestamp` | 后端推送时间戳,毫秒 |
|
| `data.dockStatus` | 自动回充状态文本;用于前端回显当前回充阶段 |
|
||||||
|
| `data.dockActive` | 回充或脱离充电桩是否进行中;为 `true` 时前端禁用「自动回充」「充电脱离」按钮 |
|
||||||
|
| `timestamp` | 后端推送时间戳,毫秒 |
|
||||||
|
|
||||||
|
|
||||||
## 小车速度上报消息
|
## 小车速度上报消息
|
||||||
|
|||||||
@@ -165,6 +165,74 @@ uint16 height
|
|||||||
| `3` | 停止 | `0` |
|
| `3` | 停止 | `0` |
|
||||||
| `4` | 运行到指定高度 | 目标高度,单位 mm |
|
| `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%)时,自动触发一次脱离。
|
||||||
|
|
||||||
## 机器人端收到的消息
|
## 机器人端收到的消息
|
||||||
|
|
||||||
设备端连接:
|
设备端连接:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default defineConfig(({ command }: ConfigEnv) => {
|
|||||||
rewrite: (path: string) => path.replace(/^\/api/, '')
|
rewrite: (path: string) => path.replace(/^\/api/, '')
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: `ws://192.168.3.110:16003/`,
|
target: `ws://127.0.0.1:16003/`,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true
|
ws: true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user