inspection-host-computer/server/index.js
2026-07-22 15:03:56 +08:00

549 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
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 = 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}`)
})