522 lines
13 KiB
JavaScript
522 lines
13 KiB
JavaScript
// server/index.js
|
||
import express from 'express'
|
||
import cors from 'cors'
|
||
import path from 'path';
|
||
|
||
import 'dotenv/config';
|
||
import {
|
||
createAGVGrpcClient,
|
||
createARMGrpcClient,
|
||
createMicrophoneGrpcClient,
|
||
createSpeakerGrpcClient,
|
||
__dirname
|
||
} from './grpcClient.js';
|
||
import { attachAudioWebSocket } from './audioWebSocket.js';
|
||
|
||
const app = express()
|
||
app.use(express.static(path.join(__dirname, '../dist')));
|
||
app.use(cors())
|
||
app.use(express.json())
|
||
|
||
// 获取机器人麦克风音量(0-100)。
|
||
app.post('/api/audio/microphone/volume/get', (req, res) => {
|
||
const { ip, deviceId } = req.body
|
||
if (!ip || !deviceId) {
|
||
res.status(400).json({ code: 400, message: '机器人地址和麦克风设备 ID 不能为空' })
|
||
return
|
||
}
|
||
|
||
const grpcClient = createMicrophoneGrpcClient(ip)
|
||
grpcClient.getVolume(
|
||
{ header: { device_id: deviceId } },
|
||
{ deadline: Date.now() + 5000 },
|
||
(error, feedback) => {
|
||
grpcClient.close()
|
||
if (error) {
|
||
console.error('[audio] get microphone volume failed:', error)
|
||
res.status(502).json({ code: 502, message: error.details || error.message || '获取机器人音量失败' })
|
||
return
|
||
}
|
||
if (feedback?.header?.success === false) {
|
||
res.status(502).json({
|
||
code: 502,
|
||
message: feedback.header.error_message || '机器人拒绝获取音量'
|
||
})
|
||
return
|
||
}
|
||
res.json({ code: 200, data: { volume: feedback?.volume ?? 0 } })
|
||
}
|
||
)
|
||
})
|
||
|
||
// 设置机器人麦克风音量(0-100)。
|
||
app.post('/api/audio/microphone/volume/set', (req, res) => {
|
||
const { ip, deviceId } = req.body
|
||
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) {
|
||
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
|
||
return
|
||
}
|
||
|
||
const grpcClient = createMicrophoneGrpcClient(ip)
|
||
grpcClient.setVolume({
|
||
header: { device_id: deviceId },
|
||
volume
|
||
}, { deadline: Date.now() + 5000 }, (error, feedback) => {
|
||
grpcClient.close()
|
||
if (error) {
|
||
console.error('[audio] set microphone volume failed:', error)
|
||
res.status(502).json({ code: 502, message: error.details || error.message || '设置机器人音量失败' })
|
||
return
|
||
}
|
||
if (feedback?.header?.success === false) {
|
||
res.status(502).json({
|
||
code: 502,
|
||
message: feedback.header.error_message || '机器人拒绝设置音量'
|
||
})
|
||
return
|
||
}
|
||
res.json({ code: 200, data: { volume } })
|
||
})
|
||
})
|
||
|
||
// 获取机器人扬声器音量(0-100)。
|
||
app.post('/api/audio/speaker/volume/get', (req, res) => {
|
||
const { ip, deviceId } = req.body
|
||
if (!ip || !deviceId) {
|
||
res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
|
||
return
|
||
}
|
||
|
||
const grpcClient = createSpeakerGrpcClient(ip)
|
||
grpcClient.getVolume(
|
||
{ header: { device_id: deviceId } },
|
||
{ deadline: Date.now() + 5000 },
|
||
(error, feedback) => {
|
||
grpcClient.close()
|
||
if (error) {
|
||
console.error('[audio] get speaker volume failed:', error)
|
||
res.status(502).json({ code: 502, message: error.details || error.message || '获取扬声器音量失败' })
|
||
return
|
||
}
|
||
if (feedback?.header?.success === false) {
|
||
res.status(502).json({
|
||
code: 502,
|
||
message: feedback.header.error_message || '机器人拒绝获取扬声器音量'
|
||
})
|
||
return
|
||
}
|
||
res.json({ code: 200, data: { volume: feedback?.volume ?? 0 } })
|
||
}
|
||
)
|
||
})
|
||
|
||
// 设置机器人扬声器音量(0-100)。
|
||
app.post('/api/audio/speaker/volume/set', (req, res) => {
|
||
const { ip, deviceId } = req.body
|
||
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) {
|
||
res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
|
||
return
|
||
}
|
||
|
||
const grpcClient = createSpeakerGrpcClient(ip)
|
||
grpcClient.setVolume({
|
||
header: { device_id: deviceId },
|
||
volume
|
||
}, { deadline: Date.now() + 5000 }, (error, feedback) => {
|
||
grpcClient.close()
|
||
if (error) {
|
||
console.error('[audio] set speaker volume failed:', error)
|
||
res.status(502).json({ code: 502, message: error.details || error.message || '设置扬声器音量失败' })
|
||
return
|
||
}
|
||
if (feedback?.header?.success === false) {
|
||
res.status(502).json({
|
||
code: 502,
|
||
message: feedback.header.error_message || '机器人拒绝设置扬声器音量'
|
||
})
|
||
return
|
||
}
|
||
res.json({ code: 200, data: { volume } })
|
||
})
|
||
})
|
||
|
||
// 获取agv运行时状态
|
||
app.post('/api/agv/getRuntimeState', async (req, res) => {
|
||
const request = {
|
||
header: {
|
||
device_id: req.body.deviceId || 'agv_src1100'
|
||
}
|
||
};
|
||
|
||
const grpcClient = createAGVGrpcClient(req.body.ip)
|
||
|
||
const resp = await new Promise((resolve, reject) => {
|
||
grpcClient.getRuntimeState(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/agv/getCurrentMap', async (req, res) => {
|
||
const grpcClient = createAGVGrpcClient(req.body.ip)
|
||
const request = {
|
||
header: {
|
||
device_id: req.body.deviceId || 'agv_src1100'
|
||
},
|
||
map_name: req.body.mapName
|
||
};
|
||
|
||
const resp = await new Promise((resolve, reject) => {
|
||
grpcClient.downloadMap(request, (err, res) => {
|
||
if (err) {
|
||
resolve({
|
||
code: 500,
|
||
data: `grpc服务端错误`
|
||
});
|
||
} else {
|
||
resolve({
|
||
code: 200,
|
||
data: res.content
|
||
});
|
||
}
|
||
});
|
||
})
|
||
|
||
if (resp.code === 200) {
|
||
res.json(resp)
|
||
} else {
|
||
res.status(500).json({ error: 'grpc服务端错误' })
|
||
}
|
||
})
|
||
|
||
// 机器人运动控制
|
||
app.post('/api/agv/moveRobot', async (req, res) => {
|
||
const grpcClient = setVelocity(req.body.ip)
|
||
const request = {
|
||
header: {
|
||
device_id: req.body.deviceId || 'agv_src1100'
|
||
},
|
||
velocity: {
|
||
vx: req.body.vx,
|
||
vy: req.body.vy,
|
||
vz: req.body.vz
|
||
}
|
||
};
|
||
|
||
const resp = await new Promise((resolve, reject) => {
|
||
grpcClient.setVelocity(request, (err, res) => {
|
||
if (err) {
|
||
resolve({
|
||
code: 500,
|
||
data: `grpc服务端错误`
|
||
});
|
||
} else {
|
||
resolve({
|
||
code: 200,
|
||
data: res.status
|
||
});
|
||
}
|
||
});
|
||
})
|
||
|
||
if (resp.code === 200) {
|
||
res.json({
|
||
code: 200,
|
||
data: resp
|
||
})
|
||
} else {
|
||
res.status(500).json({ error: 'grpc服务端错误' })
|
||
}
|
||
})
|
||
|
||
// 机器人停止运动
|
||
app.post('/api/agv/stopRobot', async (req, res) => {
|
||
const grpcClient = createAGVGrpcClient(req.body.ip)
|
||
const request = {
|
||
header: {
|
||
device_id: req.body.deviceId || 'agv_src1100'
|
||
}
|
||
};
|
||
|
||
const resp = await new Promise((resolve, reject) => {
|
||
grpcClient.stopVelocityControl(request, (err, res) => {
|
||
if (err) {
|
||
resolve({
|
||
code: 500,
|
||
data: `grpc服务端错误`
|
||
});
|
||
} else {
|
||
resolve({
|
||
code: 200,
|
||
data: res.status
|
||
});
|
||
}
|
||
});
|
||
})
|
||
|
||
if (resp.code === 200) {
|
||
res.json({
|
||
code: 200,
|
||
data: resp
|
||
})
|
||
} else {
|
||
res.status(500).json({ error: 'grpc服务端错误' })
|
||
}
|
||
})
|
||
|
||
|
||
// ==================================================
|
||
|
||
// 获取机械臂末端位姿
|
||
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服务端错误' })
|
||
}
|
||
})
|
||
|
||
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 PORT = process.env.VITE_SERVICE_PORT || 3000;
|
||
|
||
// ============ 生产环境:托管 Vite 构建产物 ============
|
||
if (isProd) {
|
||
const distPath = path.resolve(__dirname, '../dist');
|
||
|
||
// 托管静态文件
|
||
app.use(express.static(distPath));
|
||
|
||
// SPA 回退:所有非 API 请求返回 index.html
|
||
app.get('/{*path}', (req, res) => {
|
||
res.sendFile(path.resolve(distPath, 'index.html'));
|
||
});
|
||
|
||
app.post('/{*path}', (req, res) => {
|
||
res.sendFile(path.resolve(distPath, 'index.html'));
|
||
});
|
||
}
|
||
|
||
// ========== 启动 =========
|
||
|
||
const httpServer = app.listen(PORT, () => {
|
||
console.log(`gRPC 桥接服务已启动: http://localhost:${PORT}`)
|
||
})
|
||
|
||
attachAudioWebSocket(httpServer)
|