refactor: 按机器人动态解析gRPC端点与设备

This commit is contained in:
zhanghao 2026-08-14 17:31:21 +08:00
parent 5b294f6d37
commit 92bd36c705
13 changed files with 130 additions and 212 deletions

View File

@ -23,7 +23,7 @@ function waitForGrpcReady(client, timeoutMs = 5000) {
}) })
} }
export function attachAudioWebSocket(httpServer) { export function attachAudioWebSocket(httpServer, { resolveRobotAddress, resolveRobotDeviceId } = {}) {
const wss = new WebSocketServer({ server: httpServer, path: '/audio' }) const wss = new WebSocketServer({ server: httpServer, path: '/audio' })
wss.on('connection', (socket) => { wss.on('connection', (socket) => {
@ -101,14 +101,27 @@ export function attachAudioWebSocket(httpServer) {
} }
if (message.type !== 'init' || initialized) return if (message.type !== 'init' || initialized) return
const { microphoneId, robotAddress } = message const { robotId } = message
speakerId = message.speakerId?.trim() if (typeof robotId !== 'string' || !robotId.trim()) {
if (![microphoneId, speakerId, robotAddress].every((value) => typeof value === 'string' && value.trim())) {
sendJson(socket, { type: 'error', message: '机器人地址、麦克风 ID 和扬声器 ID 不能为空' }) sendJson(socket, { type: 'error', message: '机器人地址、麦克风 ID 和扬声器 ID 不能为空' })
socket.close(1008, 'invalid init') socket.close(1008, 'invalid init')
return return
} }
const robotAddress = resolveRobotAddress?.(robotId.trim())
if (!robotAddress) {
sendJson(socket, { type: 'error', message: `机器人 ${robotId} 不在线或尚未注册` })
socket.close(1008, 'robot unavailable')
return
}
const microphoneId = resolveRobotDeviceId?.(robotId.trim(), 'microphone')
speakerId = resolveRobotDeviceId?.(robotId.trim(), 'speaker')
if (!microphoneId || !speakerId) {
sendJson(socket, { type: 'error', message: `机器人 ${robotId} 缺少麦克风或扬声器设备` })
socket.close(1008, 'audio device unavailable')
return
}
initialized = true initialized = true
microphoneClient = createMicrophoneGrpcClient(robotAddress.trim()) microphoneClient = createMicrophoneGrpcClient(robotAddress.trim())
speakerClient = createSpeakerGrpcClient(robotAddress.trim()) speakerClient = createSpeakerGrpcClient(robotAddress.trim())

View File

@ -84,14 +84,9 @@ function message(error) {
} }
export function streamCameraVideo(req, res) { export function streamCameraVideo(req, res) {
const ip = typeof req.query.ip === 'string' ? req.query.ip.trim() : '' const deviceId = req.deviceId
const deviceId = typeof req.query.deviceId === 'string' ? req.query.deviceId.trim() : ''
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: 'ip 和 deviceId 不能为空' })
return
}
const client = createCameraGrpcClient(ip) const client = createCameraGrpcClient(req.robotAddress)
const grpcStream = client.getRgbImageStream() const grpcStream = client.getRgbImageStream()
let transcoder let transcoder
let closed = false let closed = false

View File

@ -17,7 +17,7 @@ import {
import { attachAudioWebSocket } from './audioWebSocket.js'; import { attachAudioWebSocket } from './audioWebSocket.js';
import { streamCameraVideo } from './cameraVideoStream.js'; import { streamCameraVideo } from './cameraVideoStream.js';
import { startQuicServer } from './quicServer.js'; import { startQuicServer } from './quicServer.js';
import { consumeQuicEvent, listRobots } from './robotRegistry.js'; import { consumeQuicEvent, getRobotAddress, getRobotDeviceId, listRobots } from './robotRegistry.js';
const app = express() const app = express()
app.use(express.static(path.join(__dirname, '../dist'))); app.use(express.static(path.join(__dirname, '../dist')));
@ -28,19 +28,49 @@ app.get('/api/quic/robots', (req, res) => {
res.json({ code: 200, data: listRobots() }) res.json({ code: 200, data: listRobots() })
}) })
app.use('/api', (req, res, next) => {
const robotId = String(req.body?.robotId || req.query?.robotId || '').trim()
if (!robotId) {
res.status(400).json({ code: 400, message: 'robotId 不能为空' })
return
}
const address = getRobotAddress(robotId)
if (!address) {
res.status(404).json({ code: 404, message: `机器人 ${robotId} 不在线或尚未注册` })
return
}
req.robotId = robotId
req.robotAddress = address
next()
})
app.use('/api', (req, res, next) => {
let kind = ''
if (req.path.startsWith('/agv/')) kind = 'agv'
else if (req.path.startsWith('/arm/')) kind = 'arm'
else if (req.path.startsWith('/audio/microphone/')) kind = 'microphone'
else if (req.path.startsWith('/audio/speaker/')) kind = 'speaker'
else if (req.path.startsWith('/camera/')) kind = 'camera'
if (!kind) return next()
const deviceId = getRobotDeviceId(req.robotId, kind)
if (!deviceId) {
res.status(404).json({ code: 404, message: `机器人 ${req.robotId} 没有可用的 ${kind} 设备` })
return
}
req.deviceId = deviceId
next()
})
// 将相机逐帧 gRPC 数据转码为浏览器可直接播放的 fragmented MP4。 // 将相机逐帧 gRPC 数据转码为浏览器可直接播放的 fragmented MP4。
// 示例:/api/camera/video?ip=192.168.0.28%3A50052&deviceId=camera1 // 示例:/api/camera/video?robotId=robot-1
app.get('/api/camera/video', streamCameraVideo) app.get('/api/camera/video', streamCameraVideo)
// 获取机器人麦克风音量(0-100)。 // 获取机器人麦克风音量(0-100)。
app.post('/api/audio/microphone/volume/get', (req, res) => { app.post('/api/audio/microphone/volume/get', (req, res) => {
const { ip, deviceId } = req.body const deviceId = req.deviceId
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和麦克风设备 ID 不能为空' })
return
}
const grpcClient = createMicrophoneGrpcClient(ip) const grpcClient = createMicrophoneGrpcClient(req.robotAddress)
grpcClient.getVolume( grpcClient.getVolume(
{ header: { device_id: deviceId } }, { header: { device_id: deviceId } },
{ deadline: Date.now() + 5000 }, { deadline: Date.now() + 5000 },
@ -65,18 +95,14 @@ app.post('/api/audio/microphone/volume/get', (req, res) => {
// 设置机器人麦克风音量(0-100)。 // 设置机器人麦克风音量(0-100)。
app.post('/api/audio/microphone/volume/set', (req, res) => { app.post('/api/audio/microphone/volume/set', (req, res) => {
const { ip, deviceId } = req.body const deviceId = req.deviceId
const volume = Number(req.body.volume) const volume = Number(req.body.volume)
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和麦克风设备 ID 不能为空' })
return
}
if (!Number.isInteger(volume) || volume < 0 || volume > 100) { if (!Number.isInteger(volume) || volume < 0 || volume > 100) {
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' }) res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
return return
} }
const grpcClient = createMicrophoneGrpcClient(ip) const grpcClient = createMicrophoneGrpcClient(req.robotAddress)
grpcClient.setVolume({ grpcClient.setVolume({
header: { device_id: deviceId }, header: { device_id: deviceId },
volume volume
@ -100,13 +126,9 @@ app.post('/api/audio/microphone/volume/set', (req, res) => {
// 获取机器人扬声器音量(0-100)。 // 获取机器人扬声器音量(0-100)。
app.post('/api/audio/speaker/volume/get', (req, res) => { app.post('/api/audio/speaker/volume/get', (req, res) => {
const { ip, deviceId } = req.body const deviceId = req.deviceId
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
return
}
const grpcClient = createSpeakerGrpcClient(ip) const grpcClient = createSpeakerGrpcClient(req.robotAddress)
grpcClient.getVolume( grpcClient.getVolume(
{ header: { device_id: deviceId } }, { header: { device_id: deviceId } },
{ deadline: Date.now() + 5000 }, { deadline: Date.now() + 5000 },
@ -131,18 +153,14 @@ app.post('/api/audio/speaker/volume/get', (req, res) => {
// 设置机器人扬声器音量(0-100)。 // 设置机器人扬声器音量(0-100)。
app.post('/api/audio/speaker/volume/set', (req, res) => { app.post('/api/audio/speaker/volume/set', (req, res) => {
const { ip, deviceId } = req.body const deviceId = req.deviceId
const volume = Number(req.body.volume) const volume = Number(req.body.volume)
if (!ip || !deviceId) {
res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
return
}
if (!Number.isInteger(volume) || volume < 0 || volume > 100) { if (!Number.isInteger(volume) || volume < 0 || volume > 100) {
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' }) res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
return return
} }
const grpcClient = createSpeakerGrpcClient(ip) const grpcClient = createSpeakerGrpcClient(req.robotAddress)
grpcClient.setVolume({ grpcClient.setVolume({
header: { device_id: deviceId }, header: { device_id: deviceId },
volume volume
@ -168,11 +186,11 @@ app.post('/api/audio/speaker/volume/set', (req, res) => {
app.post('/api/agv/getRuntimeState', async (req, res) => { app.post('/api/agv/getRuntimeState', async (req, res) => {
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'agv_src1100' device_id: req.deviceId
} }
}; };
const grpcClient = createAGVGrpcClient(req.body.ip) const grpcClient = createAGVGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.getRuntimeState(request, (err, res) => { grpcClient.getRuntimeState(request, (err, res) => {
@ -199,10 +217,10 @@ app.post('/api/agv/getRuntimeState', async (req, res) => {
// 获取机器人当前地图 // 获取机器人当前地图
app.post('/api/agv/getCurrentMap', async (req, res) => { app.post('/api/agv/getCurrentMap', async (req, res) => {
const grpcClient = createAGVGrpcClient(req.body.ip) const grpcClient = createAGVGrpcClient(req.robotAddress)
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'agv_src1100' device_id: req.deviceId
}, },
map_name: req.body.mapName map_name: req.body.mapName
}; };
@ -232,10 +250,10 @@ app.post('/api/agv/getCurrentMap', async (req, res) => {
// 机器人运动控制 // 机器人运动控制
app.post('/api/agv/moveRobot', async (req, res) => { app.post('/api/agv/moveRobot', async (req, res) => {
const grpcClient = createAGVGrpcClient(req.body.ip) const grpcClient = createAGVGrpcClient(req.robotAddress)
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'agv_src1100' device_id: req.deviceId
}, },
velocity: { velocity: {
vx: req.body.vx, vx: req.body.vx,
@ -272,10 +290,10 @@ app.post('/api/agv/moveRobot', async (req, res) => {
// 机器人停止运动 // 机器人停止运动
app.post('/api/agv/stopRobot', async (req, res) => { app.post('/api/agv/stopRobot', async (req, res) => {
const grpcClient = createAGVGrpcClient(req.body.ip) const grpcClient = createAGVGrpcClient(req.robotAddress)
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'agv_src1100' device_id: req.deviceId
} }
}; };
@ -312,11 +330,11 @@ app.post('/api/agv/stopRobot', async (req, res) => {
app.post('/api/arm/getPose', async (req, res) => { app.post('/api/arm/getPose', async (req, res) => {
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'huayan_arm' device_id: req.deviceId
} }
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.getPose(request, (err, res) => { grpcClient.getPose(request, (err, res) => {
@ -345,11 +363,11 @@ app.post('/api/arm/getPose', async (req, res) => {
app.post('/api/arm/getJointState', async (req, res) => { app.post('/api/arm/getJointState', async (req, res) => {
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'huayan_arm' device_id: req.deviceId
} }
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.getJointState(request, (err, res) => { grpcClient.getJointState(request, (err, res) => {
@ -378,7 +396,7 @@ app.post('/api/arm/getJointState', async (req, res) => {
app.post('/api/arm/speedJ', async (req, res) => { app.post('/api/arm/speedJ', async (req, res) => {
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'huayan_arm' device_id: req.deviceId
}, },
velocity: { velocity: {
velocity: req.body.velocities velocity: req.body.velocities
@ -386,7 +404,7 @@ app.post('/api/arm/speedJ', async (req, res) => {
acceleration: req.body.acceleration, acceleration: req.body.acceleration,
duration: req.body.duration duration: req.body.duration
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.speedJ(request, (err, res) => { grpcClient.speedJ(request, (err, res) => {
@ -414,13 +432,13 @@ app.post('/api/arm/speedJ', async (req, res) => {
app.post('/api/arm/speedL', async (req, res) => { app.post('/api/arm/speedL', async (req, res) => {
const request = { const request = {
header: { header: {
device_id: req.body.deviceId || 'huayan_arm' device_id: req.deviceId
}, },
velocity: req.body.velocity, velocity: req.body.velocity,
acceleration: req.body.acceleration, acceleration: req.body.acceleration,
duration: req.body.duration duration: req.body.duration
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.speedL(request, (err, res) => { grpcClient.speedL(request, (err, res) => {
@ -449,10 +467,10 @@ app.post('/api/arm/speedL', async (req, res) => {
app.post('/api/arm/stopMotion', async (req, res) => { app.post('/api/arm/stopMotion', async (req, res) => {
const request = { const request = {
// header: { // header: {
device_id: req.body.deviceId || 'huayan_arm', device_id: req.deviceId,
// }, // },
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.stopMotion(request, (err, res) => { grpcClient.stopMotion(request, (err, res) => {
@ -479,9 +497,9 @@ app.post('/api/arm/stopMotion', async (req, res) => {
app.post('/api/arm/torqueOn', async (req, res) => { app.post('/api/arm/torqueOn', async (req, res) => {
const request = { const request = {
device_id: req.body.deviceId || 'huayan_arm', device_id: req.deviceId,
}; };
const grpcClient = createARMGrpcClient(req.body.ip) const grpcClient = createARMGrpcClient(req.robotAddress)
const resp = await new Promise((resolve, reject) => { const resp = await new Promise((resolve, reject) => {
grpcClient.torqueOn(request, (err, res) => { grpcClient.torqueOn(request, (err, res) => {
@ -551,7 +569,10 @@ if (httpsEnabled) {
httpServer = http.createServer(app); httpServer = http.createServer(app);
} }
attachAudioWebSocket(httpServer) attachAudioWebSocket(httpServer, {
resolveRobotAddress: getRobotAddress,
resolveRobotDeviceId: getRobotDeviceId
})
startQuicServer({ onEvent: consumeQuicEvent }) startQuicServer({ onEvent: consumeQuicEvent })
httpServer.listen(PORT, '0.0.0.0', () => { httpServer.listen(PORT, '0.0.0.0', () => {

View File

@ -82,3 +82,12 @@ export function listRobots() {
return [...robots.values()].sort((a, b) => a.robotId.localeCompare(b.robotId)) return [...robots.values()].sort((a, b) => a.robotId.localeCompare(b.robotId))
} }
export function getRobotAddress(robotId) {
if (!robotId) return ''
return robots.get(robotId)?.ip || ''
}
export function getRobotDeviceId(robotId, kind) {
const devices = robots.get(robotId)?.devices?.[kind] || []
return (devices.find(device => device.enabled) || devices[0])?.deviceId || ''
}

View File

@ -3,7 +3,7 @@ import request from '@/utils/request'
/** /**
* 机器人前往目标站点 * 机器人前往目标站点
* @param {Object} params * @param {Object} params
* @param {string} [params.deviceId] - 设备ID,默认 agv_src1100 * robotId 由统一请求层从 Pinia 自动附加,设备 ID 由后端注册表解析。
*/ */
export function getRuntimeState(params) { export function getRuntimeState(params) {
return request('/api/agv/getRuntimeState', params, 'POST') return request('/api/agv/getRuntimeState', params, 'POST')
@ -23,4 +23,4 @@ export function moveRobot(params) {
export function stopRobot(params) { export function stopRobot(params) {
return request('/api/agv/stopRobot', params, 'POST') return request('/api/agv/stopRobot', params, 'POST')
} }

View File

@ -3,13 +3,8 @@ import { defineStore } from 'pinia'
// 唯一id:robot // 唯一id:robot
export const useRobotStore = defineStore('robot', { export const useRobotStore = defineStore('robot', {
state: () => ({ state: () => ({
ip: '192.168.0.28:50052', robotId: '',
robotType: 'inspection', robotType: 'inspection',
agvDeviceId:'src1100',
almDeviceId: 'huayan_arm',
spkDeviceId: 'spk1',
micDeviceId: 'mic1',
cameraDeviceId: 'real_cam1',
position: { x: 0, y: 0, theta: 0 }, position: { x: 0, y: 0, theta: 0 },
battery: { battery: {
percentage: 0.99, percentage: 0.99,
@ -30,27 +25,12 @@ export const useRobotStore = defineStore('robot', {
} }
}, },
actions: { actions: {
setIp(str) { setRobotId(str) {
this.ip = str this.robotId = str
}, },
setRobotType(str) { setRobotType(str) {
this.robotType = str this.robotType = str
}, },
setAgvDeviceId(str) {
this.agvDeviceId = str
},
setAlmDeviceId(str) {
this.almDeviceId = str
},
setSpkDeviceId(str) {
this.spkDeviceId = str
},
setMicDeviceId(str) {
this.micDeviceId = str
},
setCameraDeviceId(str) {
this.cameraDeviceId = str
},
// 更新机器人位置 // 更新机器人位置
setPosition(data = {}) { setPosition(data = {}) {
this.position = data this.position = data

View File

@ -1,4 +1,5 @@
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRobotStore } from '@/stores/robot'
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 controller = new AbortController() const controller = new AbortController()
@ -9,13 +10,15 @@ const request = async (url, params = {}, method = 'GET', headers = { 'Content-Ty
}, 5000) }, 5000)
try { try {
const robotId = useRobotStore().robotId
const requestParams = robotId && !params.robotId ? { ...params, robotId } : params
const options = { const options = {
method, method,
headers, headers,
signal signal
} }
if (!['GET', 'HEAD'].includes(method.toUpperCase())) { if (!['GET', 'HEAD'].includes(method.toUpperCase())) {
options.body = JSON.stringify(params) options.body = JSON.stringify(requestParams)
} }
const res = await fetch(url, options) const res = await fetch(url, options)

View File

@ -16,7 +16,7 @@
<el-option <el-option
v-for="robot in robots" v-for="robot in robots"
:key="robot.robotId" :key="robot.robotId"
:label="`${robot.robotId}(${robot.ip})`" :label="robot.robotId"
:value="robot.robotId" :value="robot.robotId"
/> />
</el-select> </el-select>
@ -28,30 +28,12 @@
<el-table-column prop="health" label="健康状态" width="190" /> <el-table-column prop="health" label="健康状态" width="190" />
</el-table> </el-table>
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="auto"> <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="auto">
<el-form-item label="IP和端口" prop="ip">
<el-input v-model="ruleForm.ip" placeholder="请输入IP和端口" />
</el-form-item>
<el-form-item label="机器人类型" prop="robotType"> <el-form-item label="机器人类型" prop="robotType">
<el-select v-model="ruleForm.robotType" placeholder="请选择机器人类型"> <el-select v-model="ruleForm.robotType" placeholder="请选择机器人类型">
<el-option label="巡检机器人" value="inspection"></el-option> <el-option label="巡检机器人" value="inspection"></el-option>
<el-option label="讲解机器人" value="explanation"></el-option> <el-option label="讲解机器人" value="explanation"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item label="底盘设备ID" prop="agvDeviceId">
<el-input v-model="ruleForm.agvDeviceId" placeholder="请输入底盘设备ID" />
</el-form-item>
<el-form-item label="机械臂设备ID" prop="almDeviceId">
<el-input v-model="ruleForm.almDeviceId" placeholder="请输入机械臂设备ID" />
</el-form-item>
<el-form-item label="扬声器设备ID" prop="spkDeviceId">
<el-input v-model="ruleForm.spkDeviceId" placeholder="请输入扬声器设备ID" />
</el-form-item>
<el-form-item label="麦克风设备ID" prop="micDeviceId">
<el-input v-model="ruleForm.micDeviceId" placeholder="请输入麦克风设备ID" />
</el-form-item>
<el-form-item label="相机设备ID" prop="cameraDeviceId">
<el-input v-model="ruleForm.cameraDeviceId" placeholder="请输入相机设备ID" />
</el-form-item> -->
</el-form> </el-form>
<el-button :loading="loading" @click="testConnect" class="connect">进入上位机控制页面</el-button> <el-button :loading="loading" @click="testConnect" class="connect">进入上位机控制页面</el-button>
</div> </div>
@ -74,7 +56,7 @@ const robotStore = useRobotStore()
const loading = ref(false) const loading = ref(false)
const robotListLoading = ref(false) const robotListLoading = ref(false)
const robots = ref([]) const robots = ref([])
const selectedRobotId = ref('') const selectedRobotId = ref(robotStore.robotId || '')
const selectedRobot = computed(() => robots.value.find(robot => robot.robotId === selectedRobotId.value)) const selectedRobot = computed(() => robots.value.find(robot => robot.robotId === selectedRobotId.value))
const selectedDevices = computed(() => Object.entries(selectedRobot.value?.devices || {}).flatMap( const selectedDevices = computed(() => Object.entries(selectedRobot.value?.devices || {}).flatMap(
([kind, devices]) => devices.map(device => ({ kind, ...device })) ([kind, devices]) => devices.map(device => ({ kind, ...device }))
@ -82,51 +64,19 @@ const selectedDevices = computed(() => Object.entries(selectedRobot.value?.devic
let refreshTimer let refreshTimer
const ruleFormRef = ref() const ruleFormRef = ref()
const ruleForm = ref({ const ruleForm = ref({
ip: '',
robotType: 'inspection', robotType: 'inspection',
// agvDeviceId:'src1100',
// almDeviceId: 'huayan_arm',
// spkDeviceId: 'spk1',
// micDeviceId: 'mic1',
// cameraDeviceId: 'real_cam1'
}) })
const rules = ref({ const rules = ref({
ip: [
{ required: true, message: '请输入机器人IP和端口', trigger: 'blur' }
],
robotType: [ robotType: [
{ required: true, message: '请选择机器人类型', trigger: 'blur' } { required: true, message: '请选择机器人类型', trigger: 'blur' }
], ],
// agvDeviceId: [
// { required: true, message: '请输入底盘设备ID', trigger: 'blur' }
// ],
// almDeviceId: [
// { required: true, message: '请输入机械臂设备ID', trigger: 'blur' }
// ],
// spkDeviceId: [
// { required: true, message: '请输入扬声器设备ID', trigger: 'blur' }
// ],
// micDeviceId: [
// { required: true, message: '请输入麦克风设备ID', trigger: 'blur' }
// ],
// cameraDeviceId: [
// { required: true, message: '请输入相机设备ID', trigger: 'blur' }
// ],
}) })
const firstDeviceId = (robot, kind, fallback) => robot?.devices?.[kind]?.[0]?.deviceId || fallback
const selectRobot = () => { const selectRobot = () => {
const robot = selectedRobot.value const robot = selectedRobot.value
if (!robot) return if (!robot) return
ruleForm.value.ip = robot.ip robotStore.setRobotId(robot.robotId)
robotStore.setIp(robot.ip)
robotStore.setAgvDeviceId(firstDeviceId(robot, 'agv', 'src1100'))
robotStore.setAlmDeviceId(firstDeviceId(robot, 'arm', 'huayan_arm'))
robotStore.setSpkDeviceId(firstDeviceId(robot, 'speaker', 'spk1'))
robotStore.setMicDeviceId(firstDeviceId(robot, 'microphone', 'mic1'))
robotStore.setCameraDeviceId(firstDeviceId(robot, 'camera', 'real_cam1'))
} }
const loadRobots = async () => { const loadRobots = async () => {
@ -147,32 +97,19 @@ onMounted(() => {
onUnmounted(() => clearInterval(refreshTimer)) onUnmounted(() => clearInterval(refreshTimer))
const testConnect = async () => { const testConnect = async () => {
if (!selectedRobot.value) {
ElMessage.warning('请先选择已连接机器人')
return
}
const valid = await ruleFormRef.value.validate(); const valid = await ruleFormRef.value.validate();
if (valid) { if (valid) {
loading.value = true loading.value = true
robotStore.setIp(ruleForm.value.ip) robotStore.setRobotId(selectedRobot.value.robotId)
robotStore.setRobotType(ruleForm.value.robotType) robotStore.setRobotType(ruleForm.value.robotType)
if (selectedRobot.value) { selectRobot()
selectRobot()
} else if (ruleForm.value.robotType === 'inspection') {
robotStore.setAgvDeviceId('src1100')
robotStore.setAlmDeviceId('huayan_arm')
robotStore.setSpkDeviceId('spk1')
robotStore.setMicDeviceId('mic1')
robotStore.setCameraDeviceId('real_cam1')
} else if (ruleForm.value.robotType === 'explanation') {
robotStore.setAgvDeviceId('src1100')
robotStore.setAlmDeviceId('huayan_arm')
robotStore.setSpkDeviceId('spk1')
robotStore.setMicDeviceId('mic1')
robotStore.setCameraDeviceId('real_cam1')
}
try { try {
const result = await getRuntimeState({ const result = await getRuntimeState()
ip: robotStore.ip,
deviceId: robotStore.agvDeviceId
})
loading.value = false loading.value = false
if (result.code === 200) { if (result.code === 200) {
robotStore.setPosition(result.data.pose) robotStore.setPosition(result.data.pose)

View File

@ -303,7 +303,6 @@ 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 = robotStore.almDeviceId
const urdfViewerRef = ref() const urdfViewerRef = ref()
@ -343,17 +342,11 @@ const tcp = ref({
}) })
const torqueOn = async () => { const torqueOn = async () => {
await torqueOnApi({ await torqueOnApi()
ip: robotStore.ip,
deviceId: deviceId
})
} }
const getPose = async () => { const getPose = async () => {
const res = await getPoseApi({ const res = await getPoseApi()
ip: robotStore.ip,
deviceId: deviceId
})
if (res.code === 200) { if (res.code === 200) {
const { x, y, z, rx, ry, rz } = res.data const { x, y, z, rx, ry, rz } = res.data
tcp.value = { x, y, z, rx, ry, rz } tcp.value = { x, y, z, rx, ry, rz }
@ -375,10 +368,7 @@ const jointNamsMap = {
*/ */
const getJointState = async (robot) => { const getJointState = async (robot) => {
try { try {
const res = await getJointStateApi({ const res = await getJointStateApi()
deviceId: deviceId,
ip: robotStore.ip,
})
if (res.code === 200) { if (res.code === 200) {
setTimeout(() => { setTimeout(() => {
const names = res.data.name const names = res.data.name
@ -404,8 +394,6 @@ const updateJoint = async (jointName, _dir) => {
const index = jointControls.value.findIndex(joint => joint.name === jointName) const index = jointControls.value.findIndex(joint => joint.name === jointName)
velocities[index] = jointSpeed.value * (_dir === '+' ? 1 : -1) velocities[index] = jointSpeed.value * (_dir === '+' ? 1 : -1)
const res = await speedJApi({ const res = await speedJApi({
deviceId: deviceId,
ip: robotStore.ip,
acceleration: jointSpeed.value * (_dir === '+' ? 1 : -1) + 0.1, acceleration: jointSpeed.value * (_dir === '+' ? 1 : -1) + 0.1,
duration: 60, duration: 60,
velocities: velocities velocities: velocities
@ -475,8 +463,6 @@ const move = async (axis, direction, type) => {
} }
speedLApi({ speedLApi({
deviceId: deviceId,
ip: robotStore.ip,
duration: 60, duration: 60,
acceleration: type === 'posture' ? postureSpeed.value + 0.1 : endSpeed.value + 0.1, acceleration: type === 'posture' ? postureSpeed.value + 0.1 : endSpeed.value + 0.1,
velocity: { velocity: {
@ -489,10 +475,7 @@ const move = async (axis, direction, type) => {
* 停止机械臂移动 * 停止机械臂移动
*/ */
const stop = async () => { const stop = async () => {
await stopMotionApi({ await stopMotionApi()
deviceId: deviceId,
ip: robotStore.ip
})
} }
const activePointerIds = new Set() const activePointerIds = new Set()

View File

@ -151,14 +151,11 @@ const statusMeta = computed(() => {
}) })
function microphoneVolumeRequestParams() { function microphoneVolumeRequestParams() {
return { return {}
ip: robotStore.ip,
deviceId: robotStore.micDeviceId
}
} }
async function loadMicrophoneVolume({ silent = false } = {}) { async function loadMicrophoneVolume({ silent = false } = {}) {
if (!robotStore.ip || !robotStore.micDeviceId) return if (!robotStore.robotId) return
isMicrophoneVolumeLoading.value = true isMicrophoneVolumeLoading.value = true
try { try {
const response = await getMicrophoneVolume(microphoneVolumeRequestParams()) const response = await getMicrophoneVolume(microphoneVolumeRequestParams())
@ -194,14 +191,11 @@ async function updateMicrophoneVolume(volume) {
} }
function speakerVolumeRequestParams() { function speakerVolumeRequestParams() {
return { return {}
ip: robotStore.ip,
deviceId: robotStore.spkDeviceId
}
} }
async function loadSpeakerVolume({ silent = false } = {}) { async function loadSpeakerVolume({ silent = false } = {}) {
if (!robotStore.ip || !robotStore.spkDeviceId) return if (!robotStore.robotId) return
isSpeakerVolumeLoading.value = true isSpeakerVolumeLoading.value = true
try { try {
const response = await getSpeakerVolume(speakerVolumeRequestParams()) const response = await getSpeakerVolume(speakerVolumeRequestParams())
@ -391,9 +385,7 @@ function connectAudioServer(sessionToken) {
const timeout = window.setTimeout(() => reject(new Error('连接音频服务超时')), JOIN_TIMEOUT) const timeout = window.setTimeout(() => reject(new Error('连接音频服务超时')), JOIN_TIMEOUT)
socket.onopen = () => sendControlMessage({ socket.onopen = () => sendControlMessage({
type: 'init', type: 'init',
microphoneId: robotStore.micDeviceId, robotId: robotStore.robotId
speakerId: robotStore.spkDeviceId,
robotAddress: robotStore.ip
}) })
socket.onmessage = (event) => { socket.onmessage = (event) => {
if (typeof event.data !== 'string') { if (typeof event.data !== 'string') {
@ -455,7 +447,7 @@ async function startCall() {
errorMessage.value = '' errorMessage.value = ''
const sessionToken = ++callSessionId const sessionToken = ++callSessionId
try { try {
if (!robotStore.ip || !robotStore.micDeviceId || !robotStore.spkDeviceId) { if (!robotStore.robotId) {
throw new Error('请先配置机器人地址、麦克风 ID 和扬声器 ID') throw new Error('请先配置机器人地址、麦克风 ID 和扬声器 ID')
} }
// 必须在按钮点击的用户手势中创建,以符合浏览器自动播放策略。 // 必须在按钮点击的用户手势中创建,以符合浏览器自动播放策略。

View File

@ -14,20 +14,10 @@ import VoiceConversation from './VoiceConversation.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useRobotStore } from '@/stores/robot'; import { useRobotStore } from '@/stores/robot';
const VIDEO_SERVER_HOST = import.meta.env.VITE_VIDEO_SERVER_HOST || 'http://localhost:8080/'
const TerminalId = import.meta.env.VITE_TERMINAL_ID || 'c2f8a06826baf03843925c1a2a13bcfd'
const robotStore = useRobotStore() const robotStore = useRobotStore()
const videoUrl = `${VIDEO_SERVER_HOST}/api/edge/camera/rgb-stream/public?terminalId=${TerminalId}&deviceId=${robotStore.cameraDeviceId}` const videoUrl = computed(() => robotStore.robotId
? `/api/camera/video?robotId=${encodeURIComponent(robotStore.robotId)}`
// const videoUrl = computed(() => { : '')
// if (!robotStore.ip || !robotStore.cameraDeviceId) return ''
// const params = new URLSearchParams({
// ip: robotStore.ip,
// deviceId: robotStore.cameraDeviceId
// })
// return `/api/camera/video?${params}`
// })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.page-container { .page-container {

View File

@ -194,8 +194,6 @@ const run = async (data) => {
return return
} }
const res = await moveRobot({ const res = await moveRobot({
ip: robotStore.ip,
deviceId: robotStore.agvDeviceId,
...data ...data
}) })
if (res.code === 200) { if (res.code === 200) {
@ -210,8 +208,7 @@ function onRelease() {
moveRobotData.value.vx = 0; moveRobotData.value.vx = 0;
moveRobotData.value.vy = 0; moveRobotData.value.vy = 0;
// stopRobot({ // stopRobot({
// ip: robotStore.ip, // robotId: robotStore.robotId,
// deviceId: robotStore.agvDeviceId,
// }) // })
} }

View File

@ -487,8 +487,6 @@ const loadSourceMap = async (mapName) => {
resizeCanvasToContainer() resizeCanvasToContainer()
const res = await getCurrentMap({ const res = await getCurrentMap({
ip: robotStore.ip,
deviceId: robotStore.agvDeviceId,
mapName: mapName mapName: mapName
}) })
if (res.code === 200) { if (res.code === 200) {
@ -540,7 +538,7 @@ watch(() => props.mapName,
async (newVal) => { async (newVal) => {
if (newVal) { if (newVal) {
await loadSourceMap(newVal) await loadSourceMap(newVal)
addRobot(robotStore.ip, robotStore.ip, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot')) addRobot(robotStore.robotId, robotStore.robotId, robotStore.position.x, robotStore.position.y, robotStore.position.theta, getThemeColor('--color-canvas-robot'))
startRendering() startRendering()
} }
}, { }, {