inspection-host-computer/server/index.js

554 lines
14 KiB
JavaScript
Raw Normal View History

2026-06-26 10:51:01 +08:00
// server/index.js
import express from 'express'
import cors from 'cors'
2026-06-29 17:29:36 +08:00
import path from 'path';
2026-07-22 15:03:56 +08:00
import fs from 'fs';
import http from 'http';
import https from 'https';
2026-06-26 10:51:01 +08:00
2026-07-02 16:21:38 +08:00
import 'dotenv/config';
2026-07-22 13:53:44 +08:00
import {
createAGVGrpcClient,
createARMGrpcClient,
createMicrophoneGrpcClient,
2026-07-22 14:01:38 +08:00
createSpeakerGrpcClient,
2026-07-22 13:53:44 +08:00
__dirname
} from './grpcClient.js';
2026-07-22 13:33:59 +08:00
import { attachAudioWebSocket } from './audioWebSocket.js';
2026-07-31 10:44:15 +08:00
import { streamCameraVideo } from './cameraVideoStream.js';
2026-06-26 10:51:01 +08:00
const app = express()
app.use(express.static(path.join(__dirname, '../dist')));
app.use(cors())
app.use(express.json())
2026-07-31 10:44:15 +08:00
// 将相机逐帧 gRPC 数据转码为浏览器可直接播放的 fragmented MP4。
// 示例:/api/camera/video?ip=192.168.0.28%3A50052&deviceId=camera1
app.get('/api/camera/video', streamCameraVideo)
2026-07-22 13:53:44 +08:00
// 获取机器人麦克风音量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 } })
})
})
2026-07-22 14:01:38 +08:00
// 获取机器人扬声器音量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 } })
})
})
2026-07-10 13:44:07 +08:00
// 获取agv运行时状态
app.post('/api/agv/getRuntimeState', async (req, res) => {
2026-06-26 10:51:01 +08:00
const request = {
header: {
2026-07-02 16:26:02 +08:00
device_id: req.body.deviceId || 'agv_src1100'
2026-06-26 10:51:01 +08:00
}
};
2026-07-02 16:21:38 +08:00
const grpcClient = createAGVGrpcClient(req.body.ip)
2026-06-26 10:51:01 +08:00
const resp = await new Promise((resolve, reject) => {
2026-07-10 13:44:07 +08:00
grpcClient.getRuntimeState(request, (err, res) => {
2026-06-26 10:51:01 +08:00
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
2026-07-10 13:44:07 +08:00
data: res.state
2026-06-26 10:51:01 +08:00
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
2026-07-10 13:44:07 +08:00
// 获取机器人当前地图
2026-06-26 10:51:01 +08:00
app.post('/api/agv/getCurrentMap', async (req, res) => {
2026-07-02 16:21:38 +08:00
const grpcClient = createAGVGrpcClient(req.body.ip)
2026-06-26 10:51:01 +08:00
const request = {
header: {
2026-07-02 16:26:02 +08:00
device_id: req.body.deviceId || 'agv_src1100'
2026-06-26 10:51:01 +08:00
},
2026-07-10 13:44:07 +08:00
map_name: req.body.mapName
2026-06-26 10:51:01 +08:00
};
const resp = await new Promise((resolve, reject) => {
2026-07-10 13:44:07 +08:00
grpcClient.downloadMap(request, (err, res) => {
2026-06-26 10:51:01 +08:00
if (err) {
resolve({
code: 500,
data: `grpc服务端错误`
});
} else {
resolve({
code: 200,
2026-07-10 13:44:07 +08:00
data: res.content
2026-06-26 10:51:01 +08:00
});
}
});
})
if (resp.code === 200) {
res.json(resp)
} else {
res.status(500).json({ error: 'grpc服务端错误' })
}
})
2026-07-02 16:21:38 +08:00
// 机器人运动控制
2026-06-26 10:51:01 +08:00
app.post('/api/agv/moveRobot', async (req, res) => {
2026-08-03 16:52:27 +08:00
const grpcClient = createAGVGrpcClient(req.body.ip)
2026-06-26 10:51:01 +08:00
const request = {
header: {
2026-07-02 16:26:02 +08:00
device_id: req.body.deviceId || 'agv_src1100'
2026-06-26 10:51:01 +08:00
},
2026-07-10 13:44:07 +08:00
velocity: {
2026-06-26 10:51:01 +08:00
vx: req.body.vx,
vy: req.body.vy,
2026-07-10 13:44:07 +08:00
vz: req.body.vz
2026-06-26 10:51:01 +08:00
}
};
const resp = await new Promise((resolve, reject) => {
2026-07-10 13:44:07 +08:00
grpcClient.setVelocity(request, (err, res) => {
2026-06-26 10:51:01 +08:00
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服务端错误' })
}
})
2026-07-02 16:21:38 +08:00
// 机器人停止运动
2026-06-26 10:51:01 +08:00
app.post('/api/agv/stopRobot', async (req, res) => {
2026-07-02 16:21:38 +08:00
const grpcClient = createAGVGrpcClient(req.body.ip)
2026-06-26 10:51:01 +08:00
const request = {
header: {
2026-07-02 16:26:02 +08:00
device_id: req.body.deviceId || 'agv_src1100'
2026-06-26 10:51:01 +08:00
}
};
const resp = await new Promise((resolve, reject) => {
2026-07-10 13:44:07 +08:00
grpcClient.stopVelocityControl(request, (err, res) => {
2026-06-26 10:51:01 +08:00
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服务端错误' })
}
})
2026-07-10 13:44:07 +08:00
// ==================================================
2026-07-02 16:21:38 +08:00
// 获取机械臂末端位姿
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服务端错误' })
}
})
2026-07-06 10:32:35 +08:00
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服务端错误' })
}
})
2026-06-29 11:17:08 +08:00
const isProd = process.env.NODE_ENV === 'production';
2026-07-22 15:03:56 +08:00
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}`);
}
2026-06-29 11:17:08 +08:00
// ============ 生产环境:托管 Vite 构建产物 ============
if (isProd) {
2026-06-29 17:29:36 +08:00
const distPath = path.resolve(__dirname, '../dist');
2026-06-29 11:17:08 +08:00
// 托管静态文件
app.use(express.static(distPath));
// SPA 回退:所有非 API 请求返回 index.html
2026-06-29 17:33:18 +08:00
app.get('/{*path}', (req, res) => {
2026-06-29 17:29:36 +08:00
res.sendFile(path.resolve(distPath, 'index.html'));
2026-06-29 11:17:08 +08:00
});
2026-06-29 17:33:18 +08:00
app.post('/{*path}', (req, res) => {
2026-06-29 17:29:36 +08:00
res.sendFile(path.resolve(distPath, 'index.html'));
2026-06-29 11:17:08 +08:00
});
}
// ========== 启动 =========
2026-06-26 10:51:01 +08:00
2026-07-22 15:03:56 +08:00
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);
}
2026-07-22 13:33:59 +08:00
attachAudioWebSocket(httpServer)
2026-07-22 15:03:56 +08:00
httpServer.listen(PORT, '0.0.0.0', () => {
const protocol = httpsEnabled ? 'https' : 'http';
console.log(`gRPC 桥接服务已启动: ${protocol}://0.0.0.0:${PORT}`)
})