// server/index.js import express from 'express' import cors from 'cors' import path from 'path'; import fs from 'fs'; import http from 'http'; import https from 'https'; import 'dotenv/config'; import { createAGVGrpcClient, createARMGrpcClient, createMicrophoneGrpcClient, createSpeakerGrpcClient, __dirname } from './grpcClient.js'; import { attachAudioWebSocket } from './audioWebSocket.js'; import { streamCameraVideo } from './cameraVideoStream.js'; const app = express() app.use(express.static(path.join(__dirname, '../dist'))); app.use(cors()) app.use(express.json()) // 将相机逐帧 gRPC 数据转码为浏览器可直接播放的 fragmented MP4。 // 示例:/api/camera/video?ip=192.168.0.28%3A50052&deviceId=camera1 app.get('/api/camera/video', streamCameraVideo) // 获取机器人麦克风音量(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 = createAGVGrpcClient(req.body.ip) const request = { header: { device_id: req.body.deviceId || 'agv_src1100' }, velocity: { vx: req.body.vx, vy: req.body.vy, wz: req.body.wz } }; 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 = Number(process.env.VITE_SERVICE_PORT || 3000); const httpsEnabled = process.env.HTTPS_ENABLED === 'true'; if (!Number.isInteger(PORT) || PORT < 1 || PORT > 65535) { throw new Error(`VITE_SERVICE_PORT 必须是 1-65535 之间的整数,当前值为: ${process.env.VITE_SERVICE_PORT}`); } // ============ 生产环境:托管 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')); }); } // ========== 启动 ========= let httpServer; if (httpsEnabled) { const certFile = process.env.HTTPS_CERT_FILE; const keyFile = process.env.HTTPS_KEY_FILE; if (!certFile || !keyFile) { throw new Error('启用 HTTPS 时必须设置 HTTPS_CERT_FILE 和 HTTPS_KEY_FILE'); } httpServer = https.createServer({ cert: fs.readFileSync(certFile), key: fs.readFileSync(keyFile) }, app); } else { httpServer = http.createServer(app); } attachAudioWebSocket(httpServer) httpServer.listen(PORT, '0.0.0.0', () => { const protocol = httpsEnabled ? 'https' : 'http'; console.log(`gRPC 桥接服务已启动: ${protocol}://0.0.0.0:${PORT}`) })