feat: 上位机首页

This commit is contained in:
zhanghao 2026-07-06 10:32:35 +08:00
parent 162a9b6e58
commit 98031a0dcc
11 changed files with 185 additions and 62 deletions

4
.env
View File

@ -1,5 +1,5 @@
VITE_AGV_DEVICE_ID = 'agv_src2200' # VITE_AGV_DEVICE_ID = 'agv_src2200'
VITE_ARM_DEVICE_ID = 'huayan_arm' # VITE_ARM_DEVICE_ID = 'aubo_arm'
# 机械臂IP 192.168.1.109:50052 # 机械臂IP 192.168.1.109:50052
VITE_SERVICE_PORT = 5175 VITE_SERVICE_PORT = 5175

1
auto-imports.d.ts vendored
View File

@ -7,6 +7,7 @@
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue').EffectScope const EffectScope: typeof import('vue').EffectScope
const ElMess: typeof import('element-plus/es').ElMess
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp const createApp: typeof import('vue').createApp

1
components.d.ts vendored
View File

@ -14,6 +14,7 @@ declare module 'vue' {
ElB: typeof import('element-plus/es')['ElB'] ElB: typeof import('element-plus/es')['ElB']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']

View File

@ -384,6 +384,35 @@ app.post('/api/arm/stopMotion', async (req, res) => {
} }
}) })
app.post('/api/arm/torqueOn', async (req, res) => {
const request = {
device_id: req.body.deviceId || 'huayan_arm',
};
const grpcClient = createARMGrpcClient(req.body.ip)
const resp = await new Promise((resolve, reject) => {
grpcClient.torqueOn(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服务端错误' })
}
})
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
const PORT = process.env.VITE_SERVICE_PORT || 3000; const PORT = process.env.VITE_SERVICE_PORT || 3000;

BIN
src/assets/address.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

View File

@ -3,7 +3,9 @@ import { defineStore } from 'pinia'
// 唯一idrobot // 唯一idrobot
export const useRobotStore = defineStore('robot', { export const useRobotStore = defineStore('robot', {
state: () => ({ state: () => ({
ip: '192.168.0.100:50052', ip: '192.168.0.109:50052',
agvDeviceId:'agv_src2200',
almDeviceId: 'aubo_arm',
position: { x: 0, y: 0, angle: 0 }, position: { x: 0, y: 0, angle: 0 },
battery: 100, battery: 100,
status: 'idle' status: 'idle'
@ -21,6 +23,12 @@ export const useRobotStore = defineStore('robot', {
setIp(str) { setIp(str) {
this.ip = str this.ip = str
}, },
setAgvDeviceId(str) {
this.agvDeviceId = str
},
setAlmDeviceId(str) {
this.almDeviceId = str
},
// 更新机器人位置 // 更新机器人位置
setPosition(x, y, angle) { setPosition(x, y, angle) {
this.position.x = x this.position.x = x

View File

@ -1,9 +1,22 @@
const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => { const request = async (url, params = {}, method = 'GET', headers = { 'Content-Type': 'application/json' }) => {
const res = await fetch(url, { // 1. 创建控制器
method: method, const controller = new AbortController();
headers: headers, const { signal } = controller;
body: JSON.stringify(params)
}) // 2. 设置定时器,超时后调用 abort()
const timeoutId = setTimeout(() => {
controller.abort(); // 触发中止信号
}, 5000)
const res = await fetch(url, {
method: method,
headers: headers,
body: JSON.stringify(params),
signal
}).finally(() => {
// 4. 无论请求成功还是失败,都要清除定时器,防止内存泄漏
clearTimeout(timeoutId);
});
const data = await res.json() const data = await res.json()
return data return data

View File

@ -1,18 +1,24 @@
<template> <template>
<div class="page-container"> <div class="page-container">
<div class="title">设备连接登录</div> <div class="card">
<div class="secondaryTitle">请输入IP和端口连接后进入上位机页面</div> <div class="title">设备连接登录</div>
<div class="form"> <div class="secondaryTitle">填写设备通讯参数建立与AGV底盘和机械臂的连接</div>
<el-form ref="ruleFormRef" style="max-width: 600px" :model="ruleForm" :rules="rules" label-width="auto"> <div class="tips">请操持设备与上位机在同一局域网内</div>
<el-form-item label="" prop="ip"> <div class="form">
<el-input v-model="ruleForm.ip" style="width: 240px" placeholder="请输入IP" > <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="auto">
<template #append> <el-form-item label="IP和端口" prop="ip">
<el-button @click="testConnect"></el-button> <el-input v-model="ruleForm.ip" placeholder="请输入IP和端口" />
</template> </el-form-item>
</el-input> <el-form-item label="底盘设备ID" prop="agvDeviceId">
<!-- <el-button @click="testConnect"></el-button> --> <el-input v-model="ruleForm.agvDeviceId" placeholder="请输入底盘设备ID" />
</el-form-item> </el-form-item>
</el-form> <el-form-item label="机械臂设备ID" prop="almDeviceId">
<el-input v-model="ruleForm.almDeviceId" placeholder="请输入机械臂设备ID" />
</el-form-item>
</el-form>
<el-button :loading="loading" @click="testConnect" class="connect">进入上位机控制页面</el-button>
</div>
<div class="error-tips">连接异常排查检查机器人状态局域网联通设备ID调谐是否匹配</div>
</div> </div>
</div> </div>
</template> </template>
@ -21,46 +27,55 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getCurrentMapName } from '@/api/agv.js' import { getCurrentMapName } from '@/api/agv.js'
import { useRobotStore } from '@/stores/robot' import { useRobotStore } from '@/stores/robot'
import { ElMessage } from 'element-plus'
const router = useRouter() const router = useRouter()
const robotStore = useRobotStore() const robotStore = useRobotStore()
const loading = ref(false)
const ruleFormRef = ref() const ruleFormRef = ref()
const ruleForm = ref({ const ruleForm = ref({
ip: '' ip: '',
agvDeviceId:'',
almDeviceId: ''
}) })
const rules = ref({ const rules = ref({
ip: [ ip: [
{ required: true, message: '请输入机器人IP和端口', trigger: 'blur' } { required: true, message: '请输入机器人IP和端口', trigger: 'blur' }
], ],
agvDeviceId: [
{ required: true, message: '请输入底盘设备ID', trigger: 'blur' }
],
almDeviceId: [
{ required: true, message: '请输入机械臂设备ID', trigger: 'blur' }
],
}) })
const testConnect = async () => { const testConnect = async () => {
robotStore.setIp(ruleForm.value.ip) const valid = await ruleFormRef.value.validate();
// const valid = await ruleFormRef.value.validate(); if (valid) {
// if (valid) { loading.value = true
// const result = await getCurrentMapName({ robotStore.setIp(ruleForm.value.ip)
// ip: robotStore.ip, robotStore.setAgvDeviceId(ruleForm.value.agvDeviceId)
// deviceId: VITE_AGV_DEVICE_ID, robotStore.setAlmDeviceId(ruleForm.value.almDeviceId)
// }) const result = await getCurrentMapName({
// if (result.code === 200) { ip: robotStore.ip,
// router.push({ deviceId: ruleForm.value.agvDeviceId
// path: '/home', })
// query: { loading.value = false
// mapName: result.data.current_map if (result.code === 200) {
// } router.push({
// }) path: '/home',
// } query: {
// } mapName: result.data.current_map
}
router.push({ })
path: '/home', } else {
query: { ElMessage.error(result?.message || '连接服务错误')
mapName: 'test'
} }
}) }
} }
</script> </script>
@ -74,16 +89,61 @@ const testConnect = async () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 20px; gap: 20px;
background-image: url(../assets/address.png);
background-repeat: no-repeat;
background-size: 100% 100%;
.title { .card {
font-size: 40px; padding: 40px;
font-weight: bold; box-sizing: border-box;
color: #000; background: #fff;
} width: 600px;
border-radius: 20px;
.secondaryTitle { .title {
font-size: 14px; font-size: 40px;
color: #8f8f8f; font-weight: bold;
color: #000;
margin-bottom: 12px;
}
.secondaryTitle {
font-size: 20px;
color: #8f8f8f;
margin-bottom: 8px;
}
.tips {
display: inline-block;
font-size: 14px;
padding: 4px 16px;
color: #003af7;
background: #b7c5f3;
border-radius: 12px;
margin-bottom: 12px;
}
:deep(.el-input__inner) {
height: 40px;
}
.connect {
width: 340px;
height: 50px;
display: inline-block;
padding: 4px 20px;
color: #fff;
background: #003af7;
border-radius: 12px;
margin-top: 20px;
}
.error-tips {
text-align: center;
font-size: 14px;
color: #8f8f8f;
margin-top: 10px;
}
} }
} }
</style> </style>

View File

@ -249,7 +249,7 @@ import { Remove, CirclePlus } from '@element-plus/icons-vue'
import { useRobotStore } from '@/stores/robot' import { useRobotStore } from '@/stores/robot'
const robotStore = useRobotStore() const robotStore = useRobotStore()
const deviceId = import.meta.env.VITE_ARM_DEVICE_ID const deviceId = robotStore.almDeviceId
const urdfViewerRef = ref() const urdfViewerRef = ref()
const jointControls = ref() const jointControls = ref()
@ -264,6 +264,13 @@ const tcp = ref({
rz: -72.937 rz: -72.937
}) })
const torqueOn = async () => {
await torqueOnApi({
ip: robotStore.ip,
deviceId: deviceId
})
}
const getPose = async () => { const getPose = async () => {
const res = await getPoseApi({ const res = await getPoseApi({
ip: robotStore.ip, ip: robotStore.ip,
@ -407,15 +414,15 @@ const timer = ref(null)
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
jointControls.value = urdfViewerRef.value.jointControls jointControls.value = urdfViewerRef.value.jointControls
stop() torqueOn()
getPose() getPose()
if (timer.value) { if (timer.value) {
clearInterval(timer.value) clearInterval(timer.value)
} }
timer.value = setInterval(() => { // timer.value = setInterval(() => {
getPose() getPose()
getJointState() getJointState()
}, 100) // }, 100)
}) })
}) })

View File

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

View File

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