feat: 机械臂联调

This commit is contained in:
zhanghao 2026-07-02 16:21:38 +08:00
parent 2d12fc3f89
commit e4058c6475
14 changed files with 777 additions and 127 deletions

3
.env
View File

@ -1,3 +1,4 @@
VITE_DEVICE_ID = 'agv_src2200'
VITE_AGV_DEVICE_ID = 'agv_src2200'
VITE_ARM_DEVICE_ID = 'huayan_arm'
VITE_SERVICE_PORT = 5175

View File

@ -6,7 +6,6 @@ services:
container_name: inspection-host-computer
ports:
- "5175:5175" # 映射端口
- "5173:5173" # 映射端口
environment:
- NODE_ENV=production
- VITE_SERVICE_PORT=5175 # 传给 Express 的环境变量

56
server/grpcClient.js Normal file
View File

@ -0,0 +1,56 @@
import path from 'path';
import { fileURLToPath } from 'url';
import grpc from '@grpc/grpc-js'
import protoLoader from '@grpc/proto-loader'
export const __filename = fileURLToPath(import.meta.url)
export const __dirname = path.dirname(__filename)
// ========== 加载 Proto ==========
const PROTO_ROOT = path.resolve(__dirname, './proto') // 指向 server/proto/
const AGV_PROTO_ENTRY = path.join(PROTO_ROOT, 'cmvr/api/agv_service.proto')
const ARM_PROTO_ENTRY = path.join(PROTO_ROOT, 'cmvr/api/arm_service.proto')
const agvPackageDefinition = protoLoader.loadSync(
AGV_PROTO_ENTRY,
{
includeDirs: [PROTO_ROOT , path.join(PROTO_ROOT, 'cmvr/api')], // 关键:让加载器从 PROTO_ROOT 查找 import
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
)
const armPackageDefinition = protoLoader.loadSync(
ARM_PROTO_ENTRY,
{
includeDirs: [PROTO_ROOT , path.join(PROTO_ROOT, 'cmvr/api')], // 关键:让加载器从 PROTO_ROOT 查找 import
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
)
const agvProto = grpc.loadPackageDefinition(agvPackageDefinition).cmvr.api
const armProto = grpc.loadPackageDefinition(armPackageDefinition).cmvr.api
export const createAGVGrpcClient = (address) => {
const grpcClient = new agvProto.AgvService(
address,
grpc.credentials.createInsecure()
)
return grpcClient
}
export const createARMGrpcClient = (address) => {
const grpcClient = new armProto.ArmService(
address,
grpc.credentials.createInsecure()
)
return grpcClient
}

View File

@ -1,46 +1,16 @@
// server/index.js
import express from 'express'
import cors from 'cors'
import grpc from '@grpc/grpc-js'
import protoLoader from '@grpc/proto-loader'
import path from 'path';
import { fileURLToPath } from 'url';
import 'dotenv/config';
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
import 'dotenv/config';
import { createAGVGrpcClient, createARMGrpcClient, __dirname } from './grpcClient.js';
const app = express()
app.use(express.static(path.join(__dirname, '../dist')));
app.use(cors())
app.use(express.json())
// ========== 加载 Proto ==========
const PROTO_ROOT = path.resolve(__dirname, './proto') // 指向 server/proto/
const PROTO_ENTRY = path.join(PROTO_ROOT, 'cmvr/api/agv_service.proto')
const packageDefinition = protoLoader.loadSync(
PROTO_ENTRY,
{
includeDirs: [PROTO_ROOT , path.join(PROTO_ROOT, 'cmvr/api')], // 关键:让加载器从 PROTO_ROOT 查找 import
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
)
const cmvrProto = grpc.loadPackageDefinition(packageDefinition).cmvr.api
const createGrpcClient = (address) => {
const grpcClient = new cmvrProto.AgvService(
address,
grpc.credentials.createInsecure()
)
return grpcClient
}
// 获取机器人当前地图名称
app.post('/api/agv/getCurrentMapName', async (req, res) => {
const request = {
@ -49,7 +19,7 @@ app.post('/api/agv/getCurrentMapName', async (req, res) => {
}
};
const grpcClient = createGrpcClient(req.body.ip)
const grpcClient = createAGVGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.getMapStatus(request, (err, res) => {
@ -76,7 +46,7 @@ app.post('/api/agv/getCurrentMapName', async (req, res) => {
// 获取机器人当前地图名称
app.post('/api/agv/getCurrentMap', async (req, res) => {
const grpcClient = createGrpcClient(req.body.ip)
const grpcClient = createAGVGrpcClient(req.body.ip)
const request = {
header: {
device_id: req.body.device_id || 'agv_src1100'
@ -109,6 +79,7 @@ app.post('/api/agv/getCurrentMap', async (req, res) => {
}
})
// 获取机器人状态
app.post('/api/agv/getRobotState', async (req, res) => {
const request = {
header: {
@ -116,7 +87,7 @@ app.post('/api/agv/getRobotState', async (req, res) => {
}
};
const grpcClient = createGrpcClient(req.body.ip)
const grpcClient = createAGVGrpcClient(req.body.ip)
const response = await new Promise((resolve, reject) => {
grpcClient.GetRobotLocation(request, (err, res) => {
@ -168,8 +139,9 @@ app.post('/api/agv/getRobotState', async (req, res) => {
}
})
// 机器人运动控制
app.post('/api/agv/moveRobot', async (req, res) => {
const grpcClient = createGrpcClient(req.body.ip)
const grpcClient = createAGVGrpcClient(req.body.ip)
const request = {
header: {
device_id: req.body.device_id || 'agv_src1100'
@ -208,8 +180,9 @@ app.post('/api/agv/moveRobot', async (req, res) => {
}
})
// 机器人停止运动
app.post('/api/agv/stopRobot', async (req, res) => {
const grpcClient = createGrpcClient(req.body.ip)
const grpcClient = createAGVGrpcClient(req.body.ip)
const request = {
header: {
device_id: req.body.device_id || 'agv_src1100'
@ -242,6 +215,175 @@ app.post('/api/agv/stopRobot', async (req, res) => {
}
})
// 获取机械臂末端位姿
app.post('/api/arm/getPose', async (req, res) => {
const request = {
header: {
device_id: req.body.deviceId || 'huayan_arm'
}
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.getPose(request, (err, res) => {
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
data: res.pose
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
// 获取机械臂节点状态
app.post('/api/arm/getJointState', async (req, res) => {
const request = {
header: {
device_id: req.body.deviceId || 'huayan_arm'
}
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.getJointState(request, (err, res) => {
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
data: res.state
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
// 机械臂关节运动
app.post('/api/arm/speedJ', async (req, res) => {
const request = {
header: {
device_id: req.body.deviceId || 'huayan_arm'
},
velocity: {
velocity: req.body.velocities
},
acceleration: req.body.acceleration,
duration: req.body.duration
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.speedJ(request, (err, res) => {
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
data: res
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
app.post('/api/arm/speedL', async (req, res) => {
const request = {
header: {
device_id: req.body.deviceId || 'huayan_arm'
},
velocity: req.body.velocity,
acceleration: req.body.acceleration,
duration: req.body.duration
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.speedL(request, (err, res) => {
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
data: res
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
// 机械臂停止运动
app.post('/api/arm/stopMotion', async (req, res) => {
const request = {
// header: {
device_id: req.body.deviceId || 'huayan_arm',
// },
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.stopMotion(request, (err, res) => {
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
data: res.success
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
const isProd = process.env.NODE_ENV === 'production';
const PORT = process.env.VITE_SERVICE_PORT || 3000;

View File

@ -0,0 +1,185 @@
syntax = "proto3";
package cmvr.api;
import "cmvr/api/common.proto";
enum ArmFrameType {
ARM_FRAME_BASE = 0;
ARM_FRAME_TOOL = 1;
ARM_FRAME_WORLD = 2;
ARM_FRAME_USER = 3;
}
message JointPositionCommand {
repeated double position = 1;
}
message JointVelocityCommand {
repeated double velocity = 1;
}
message MotionOptions {
double velocity = 1;
double acceleration = 2;
double blend_radius = 3;
double jerk = 4;
repeated double joint_velocity_limits = 5;
bool asynchronous = 6;
}
message CartesianPose {
double x = 1;
double y = 2;
double z = 3;
double rx = 4;
double ry = 5;
double rz = 6;
}
message CartesianVelocity {
double vx = 1;
double vy = 2;
double vz = 3;
double wx = 4;
double wy = 5;
double wz = 6;
}
message TransformMatrix4x4 {
double m00 = 1; double m01 = 2; double m02 = 3; double m03 = 4;
double m10 = 5; double m11 = 6; double m12 = 7; double m13 = 8;
double m20 = 9; double m21 = 10; double m22 = 11; double m23 = 12;
double m30 = 13; double m31 = 14; double m32 = 15; double m33 = 16;
}
message MoveJ {
message Request {
CommandHeader.Request header = 1;
JointPositionCommand target = 2;
MotionOptions options = 3;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message MoveL {
message Request {
CommandHeader.Request header = 1;
CartesianPose target = 2;
MotionOptions options = 3;
ArmFrameType frame = 4;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message SpeedJ {
message Request {
CommandHeader.Request header = 1;
JointVelocityCommand velocity = 2;
double acceleration = 3;
double duration = 4;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message SpeedL {
message Request {
CommandHeader.Request header = 1;
CartesianVelocity velocity = 2;
double acceleration = 3;
double duration = 4;
ArmFrameType frame = 5;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message ServoJ {
message Request {
CommandHeader.Request header = 1;
JointPositionCommand target = 2;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message JointState {
repeated string name = 1;
repeated double position = 2;
repeated double velocity = 3;
repeated double effort = 4;
double timestamp = 5;
}
message JointRequest {
CommandHeader.Request header = 1;
}
message JointResponse {
CommandHeader.Feedback header = 1;
JointState state = 2;
}
message GetPose {
message Request {
CommandHeader.Request header = 1;
string base_link = 2;
string ee_link = 3;
}
message Response {
CommandHeader.Feedback header = 1;
CartesianPose pose = 2;
}
}
message CalibrateZeroQ {
message Request {
CommandHeader.Request header = 1;
string joint_name = 2;
}
message Response {
CommandHeader.Feedback header = 1;
}
}
message GetPoseMatrix {
message Request {
CommandHeader.Request header = 1;
string base_link = 2;
string ee_link = 3;
}
message Response {
CommandHeader.Feedback header = 1;
TransformMatrix4x4 matrix = 2;
}
}
message ComputeForwardKinematics {
message Request {
CommandHeader.Request header = 1;
string base_link = 2;
string ee_link = 3;
JointPositionCommand joints = 4;
}
message Response {
CommandHeader.Feedback header = 1;
TransformMatrix4x4 matrix = 2;
}
}

View File

@ -0,0 +1,22 @@
syntax = "proto3";
package cmvr.api;
import "cmvr/api/common.proto";
import "cmvr/api/arm_command.proto";
service ArmService {
rpc torqueOff(CommandHeader.Request) returns (CommandHeader.Feedback);
rpc torqueOn(CommandHeader.Request) returns (CommandHeader.Feedback);
rpc moveJ(MoveJ.Request) returns (MoveJ.Response);
rpc moveL(MoveL.Request) returns (MoveL.Response);
rpc speedJ(SpeedJ.Request) returns (SpeedJ.Response);
rpc speedL(SpeedL.Request) returns (SpeedL.Response);
rpc servoJ(ServoJ.Request) returns (ServoJ.Response);
rpc stopMotion(CommandHeader.Request) returns (CommandHeader.Feedback);
rpc getJointState(JointRequest) returns (JointResponse);
rpc getPose(GetPose.Request) returns (GetPose.Response);
rpc calibrateZeroQ(CalibrateZeroQ.Request) returns (CalibrateZeroQ.Response);
rpc getPoseMatrix(GetPoseMatrix.Request) returns (GetPoseMatrix.Response);
rpc computeForwardKinematics(ComputeForwardKinematics.Request) returns (ComputeForwardKinematics.Response);
}

View File

@ -306,9 +306,6 @@ export class XacroAdapter {
* @returns {Promise<string>}
*/
static async loadFileFromMap(path, fileMap, workingPath) {
console.log('path', path)
console.log('fileMap', fileMap)
console.log('workingPath', workingPath)
// Clean path - remove leading slash if present
let cleanPath = path;
// if (cleanPath.startsWith('/')) {
@ -318,7 +315,6 @@ export class XacroAdapter {
// // Remove leading ./
// cleanPath = cleanPath.replace(/^\.\//, '');
cleanPath = cleanPath.split('///')[1];
console.log('cleanPath', cleanPath)
// Try different path combinations
const possiblePaths = [
cleanPath,
@ -328,13 +324,10 @@ export class XacroAdapter {
// Also try without the first directory component
cleanPath.includes('/') ? cleanPath.substring(cleanPath.indexOf('/') + 1) : cleanPath,
];
console.log('possiblePaths', possiblePaths)
// Try each path
for (const tryPath of possiblePaths) {
console.log('tryPath', tryPath)
const file = fileMap.get(tryPath);
if (file) {
console.log(12345)
const content = await file.text();
return content;
}

View File

@ -1,13 +1,4 @@
const fetchFn = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
const res = await fetch(url, {
method: method,
headers: headers,
body: JSON.stringify(params)
})
const data = await res.json()
return data
}
import request from '@/utils/request'
/**
* 机器人前往目标站点
@ -15,21 +6,21 @@ const fetchFn = async (url, params = {}, method = 'GET', headers = { 'Content-Ty
* @param {string} [params.deviceId] - 设备ID默认 agv_src1100
*/
export function getCurrentMapName(params) {
return fetchFn('/api/agv/getCurrentMapName', params, 'POST')
return request('/api/agv/getCurrentMapName', params, 'POST')
}
export function getCurrentMap(params) {
return fetchFn('/api/agv/getCurrentMap', params, 'POST')
return request('/api/agv/getCurrentMap', params, 'POST')
}
export function getRobotState(params) {
return fetchFn('/api/agv/getRobotState', params, 'POST')
return request('/api/agv/getRobotState', params, 'POST')
}
export function moveRobot(params) {
return fetchFn('/api/agv/moveRobot', params, 'POST')
return request('/api/agv/moveRobot', params, 'POST')
}
export function stopRobot(params) {
return fetchFn('/api/agv/stopRobot', params, 'POST')
return request('/api/agv/stopRobot', params, 'POST')
}

29
src/api/arm.js Normal file
View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
export function getPoseApi(params) {
return request('/api/arm/getPose', params, 'POST')
}
export function getJointStateApi(params) {
return request('/api/arm/getJointState', params, 'POST')
}
export function speedLApi(data) {
return request('/api/arm/speedL', data, 'POST')
}
export function moveJApi(data) {
return request('/api/arm/moveJ', data, 'POST')
}
export function stopMotionApi(params) {
return request('/api/arm/stopMotion', params, 'POST')
}
export function torqueOnApi(params) {
return request('/api/arm/torqueOn', params, 'POST')
}
export function speedJApi(data) {
return request('/api/arm/speedJ', data, 'POST')
}

12
src/utils/request.js Normal file
View File

@ -0,0 +1,12 @@
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
const res = await fetch(url, {
method: method,
headers: headers,
body: JSON.stringify(params)
})
const data = await res.json()
return data
}
export default request

View File

@ -28,8 +28,7 @@ const robotStore = useRobotStore()
const ruleFormRef = ref()
const ruleForm = ref({
ip: '',
port: 80
ip: ''
})
const rules = ref({
@ -39,21 +38,29 @@ const rules = ref({
})
const testConnect = async () => {
const valid = await ruleFormRef.value.validate();
if (valid) {
const result = await getCurrentMapName({
ip: robotStore.ip,
deviceId: VITE_DEVICE_ID,
})
if (result.code === 200) {
router.push({
path: '/home',
query: {
mapName: result.data.current_map
}
})
robotStore.setIp(ruleForm.value.ip)
// const valid = await ruleFormRef.value.validate();
// if (valid) {
// const result = await getCurrentMapName({
// ip: robotStore.ip,
// deviceId: VITE_AGV_DEVICE_ID,
// })
// if (result.code === 200) {
// router.push({
// path: '/home',
// query: {
// mapName: result.data.current_map
// }
// })
// }
// }
router.push({
path: '/home',
query: {
mapName: 'test'
}
}
})
}
</script>

View File

@ -42,15 +42,27 @@
<div class="title">关节控制</div>
<div v-for="joint in jointControls" :key="joint.name" class="joint-item">
<label :for="joint.name">{{ joint.name }}</label>
<el-slider v-model="joint.value" :min="joint.min" :max="joint.max" :step="0.01" @input="updateJoint(joint.name, joint.value)"/>
<span class="angle">{{ (joint.value * 180 / Math.PI).toFixed(1) }}°</span>
<el-input v-model="joint.value" readonly>
<template #prepend>
<el-button @mousedown="updateJoint(joint.name, '-')" @mouseup="stop"
@touchstart.prevent="updateJoint(joint.name, '-')" @touchend.prevent="stop">
-
</el-button>
</template>
<template #append>
<el-button @mousedown="updateJoint(joint.name, '+')" @mouseup="stop"
@touchstart.prevent="updateJoint(joint.name, '+')" @touchend.prevent="stop">
+
</el-button>
</template>
</el-input>
</div>
</div>
<div class="speed-container">
<div class="label">速度</div>
<el-slider v-model="speed" :min="1" :max="100" />
<div class="proportion">{{ speed }} %</div>
<el-slider v-model="jointSpeed" :min="0.1" :step="0.1" :max="9" />
<div class="proportion">{{ (jointSpeed / 9 * 100).toFixed(0) }} %</div>
</div>
</div>
@ -76,11 +88,17 @@
<div class="axis-slider-wrap">
<div class="axis-name">前后平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('x', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('x', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('x', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('x', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
@ -92,11 +110,17 @@
<div class="axis-slider-wrap">
<div class="axis-name">左右平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('y', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('y', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('y', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('y', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
@ -108,16 +132,27 @@
<div class="axis-slider-wrap">
<div class="axis-name">上下平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('z', '-', 'end')" @mouseup="stop"
@touchstart.prevent="move('z', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('z', '+', 'end')" @mouseup="stop"
@touchstart.prevent="move('z', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
</div>
</div>
<div class="speed-container">
<div class="label">速度</div>
<el-slider v-model="endSpeed" :min="0.1" :step="0.1" :max="4" />
<div class="proportion">{{ (endSpeed / 4 * 100).toFixed(0) }} %</div>
</div>
</div>
<div class="controller">
@ -129,11 +164,17 @@
<div class="axis-slider-wrap">
<div class="axis-name">前后平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('x', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('x', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('x', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('x', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
@ -145,11 +186,17 @@
<div class="axis-slider-wrap">
<div class="axis-name">左右平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('y', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('y', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('y', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('y', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
@ -161,16 +208,27 @@
<div class="axis-slider-wrap">
<div class="axis-name">上下平移</div>
<div>
<el-button type="primary" circle>
<el-icon size="24"><Remove /></el-icon>
<el-button type="primary" circle @mousedown="move('z', '-', 'posture')" @mouseup="stop"
@touchstart.prevent="move('z', '-')" @touchend.prevent="stop">
<el-icon size="24">
<Remove />
</el-icon>
</el-button>
<el-button type="primary" circle >
<el-icon size="24"><CirclePlus /></el-icon>
<el-button type="primary" circle @mousedown="move('z', '+', 'posture')" @mouseup="stop"
@touchstart.prevent="move('z', '+')" @touchend.prevent="stop">
<el-icon size="24">
<CirclePlus />
</el-icon>
</el-button>
</div>
</div>
</div>
</div>
<div class="speed-container">
<div class="label">速度</div>
<el-slider v-model="postureSpeed" :min="0.1" :step="0.1" :max="4" />
<div class="proportion">{{ (postureSpeed / 4 * 100).toFixed(0) }} %</div>
</div>
</div>
</div>
</div>
@ -178,9 +236,14 @@
</template>
<script setup>
import { nextTick, onMounted, ref } from 'vue'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import UrdfView from './UrdfView.vue'
import { getPoseApi, getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi, speedLApi } from '@/api/arm'
import { Remove, CirclePlus } from '@element-plus/icons-vue'
import { useRobotStore } from '@/stores/robot'
const robotStore = useRobotStore()
const deviceId = import.meta.env.VITE_ARM_DEVICE_ID
const urdfViewerRef = ref()
const jointControls = ref()
@ -195,18 +258,166 @@ const tcp = ref({
rz: -72.937
})
//
const speed = ref(56)
const updateJoint = (jointName, angle) => {
urdfViewerRef.value.updateJoint(jointName, angle)
const getPose = async () => {
const res = await getPoseApi({
ip: robotStore.ip,
deviceId: deviceId
})
if (res.code === 200) {
const { x, y, z, rx, ry, rz } = res.data
tcp.value = { x, y, z, rx, ry, rz }
}
}
const jointNamsMap = {
'joint_1': 'elfin_joint1',
'joint_2': 'elfin_joint2',
'joint_3': 'elfin_joint3',
'joint_4': 'elfin_joint4',
'joint_5': 'elfin_joint5',
'joint_6': 'elfin_joint6',
}
/**
* 获取关节状态
* @param robot
*/
const getJointState = async (robot) => {
try {
const res = await getJointStateApi({
deviceId: deviceId,
ip: robotStore.ip,
})
if (res.code === 200) {
setTimeout(() => {
const names = res.data.name
const positions = res.data.position
names.forEach((name, index) => {
urdfViewerRef.value.updateJoint(jointNamsMap[name] || name, positions[index])
})
}, 500)
} else {
console.error('获取关节状态失败:', res.message)
}
} catch (error) {
console.error('获取关节状态失败:', error)
}
}
//
const jointSpeed = ref(0.8)
const updateJoint = async (jointName, _dir) => {
const velocities = jointControls.value.map(joint => {
return 0
})
const index = jointControls.value.findIndex(joint => joint.name === jointName)
velocities[index] = jointSpeed.value * (_dir === '+' ? 1 : -1)
const res = await speedJApi({
deviceId: deviceId,
ip: robotStore.ip,
acceleration: jointSpeed.value * (_dir === '+' ? 1 : -1) + 0.1,
duration: 60,
velocities: velocities
})
}
const postureSpeed = ref(0.8)
const endSpeed = ref(0.8)
const speedData = reactive({
vx: 0,
vy: 0,
vz: 0,
wx: 0,
wy: 0,
wz: 0
})
/**
* 机械臂移动
* @param axis 坐标轴
* @param direction 方向
*/
const move = async (axis, direction, type) => {
speedData.vx = 0
speedData.vy = 0
speedData.vz = 0
speedData.wx = 0
speedData.wy = 0
speedData.wz = 0
if (type === 'end') {
//
const speedValue = endSpeed.value
switch (axis) {
case 'x':
speedData.vx = direction === '+' ? speedValue : -speedValue
break
case 'y':
speedData.vy = direction === '+' ? speedValue : -speedValue
break
case 'z':
speedData.vz = direction === '+' ? speedValue : -speedValue
break
}
} else if (type === 'posture') {
//
const jointSpeed = postureSpeed.value
switch (axis) {
case 'x':
speedData.wx = direction === '+' ? jointSpeed : -jointSpeed
break
case 'y':
speedData.wy = direction === '+' ? jointSpeed : -jointSpeed
break
case 'z':
speedData.wz = direction === '+' ? jointSpeed : -jointSpeed
break
}
}
speedLApi({
deviceId: deviceId,
ip: robotStore.ip,
duration: 60,
acceleration: type === 'posture' ? postureSpeed.value + 0.1 : endSpeed.value + 0.1,
velocity: {
...speedData
}
})
}
/**
* 停止机械臂移动
*/
const stop = async () => {
await stopMotionApi({
deviceId: deviceId,
ip: robotStore.ip
})
}
const timer = ref(null)
onMounted(() => {
nextTick(() => {
jointControls.value = urdfViewerRef.value.jointControls
stop()
getPose()
if (timer.value) {
clearInterval(timer.value)
}
timer.value = setInterval(() => {
getJointState()
}, 100)
})
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
<style lang="scss" scoped>
@ -290,17 +501,7 @@ onMounted(() => {
}
}
.speed-container {
display: flex;
align-items: center;
justify-content: space-between;
.label,
.proportion {
width: 100px;
text-align: center;
}
}
}
// 3D
@ -389,6 +590,18 @@ onMounted(() => {
}
}
}
.speed-container {
display: flex;
align-items: center;
justify-content: space-between;
.label,
.proportion {
width: 100px;
text-align: center;
}
}
}
}

View File

@ -141,7 +141,7 @@ const props = defineProps({
}
})
const VITE_DEVICE_ID = import.meta.env.VITE_DEVICE_ID
const VITE_AGV_DEVICE_ID = import.meta.env.VITE_AGV_DEVICE_ID
const robotStore = useRobotStore()
@ -186,7 +186,7 @@ const onPress = async (_dir) => {
moveRobotData.value.w = rotationAngle.value
const res = await moveRobot({
ip: robotStore.ip,
deviceId: VITE_DEVICE_ID,
deviceId: VITE_AGV_DEVICE_ID,
...moveRobotData.value
})
}
@ -196,7 +196,7 @@ function onRelease() {
moveRobotData.value.vy = 0;
stopRobot({
ip: robotStore.ip,
deviceId: VITE_DEVICE_ID
deviceId: VITE_AGV_DEVICE_ID
})
}

View File

@ -40,7 +40,7 @@ const props = defineProps({
}
})
const VITE_DEVICE_ID = import.meta.env.VITE_DEVICE_ID
const VITE_AGV_DEVICE_ID = import.meta.env.VITE_AGV_DEVICE_ID
const robotStore = useRobotStore()
@ -380,7 +380,7 @@ const centerCanvasView = () => {
const loadSourceMap = async (mapName) => {
const res = await getCurrentMap({
ip: robotStore.ip,
deviceId: VITE_DEVICE_ID,
deviceId: VITE_AGV_DEVICE_ID,
mapName: mapName
})
if (res.code === 200) {
@ -419,7 +419,7 @@ const moveRobot = (robot, dx, dy, dtheta = 0) => {
const getRobot = async () => {
const res = await getRobotState({
ip: robotStore.ip,
deviceId: VITE_DEVICE_ID,
deviceId: VITE_AGV_DEVICE_ID,
})
if (res.code === 200) {
robotStore.setPosition(res.data.x, res.data.y, res.data.angle)