2026-06-26 10:51:01 +08:00
|
|
|
// server/index.js
|
|
|
|
|
import express from 'express'
|
|
|
|
|
import cors from 'cors'
|
|
|
|
|
import grpc from '@grpc/grpc-js'
|
|
|
|
|
import protoLoader from '@grpc/proto-loader'
|
2026-06-29 17:27:32 +08:00
|
|
|
import { resolve } from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
import 'dotenv/config';
|
2026-06-26 10:51:01 +08:00
|
|
|
|
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
|
|
|
const __dirname = path.dirname(__filename)
|
|
|
|
|
|
|
|
|
|
const app = express()
|
|
|
|
|
app.use(express.static(path.join(__dirname, '../dist')));
|
|
|
|
|
app.use(cors())
|
|
|
|
|
app.use(express.json())
|
|
|
|
|
|
|
|
|
|
// ========== 加载 Proto ==========
|
|
|
|
|
const PROTO_ROOT = path.resolve(__dirname, './proto') // 指向 server/proto/
|
|
|
|
|
const PROTO_ENTRY = path.join(PROTO_ROOT, 'cmvr/api/agv_service.proto')
|
|
|
|
|
|
|
|
|
|
const packageDefinition = protoLoader.loadSync(
|
|
|
|
|
PROTO_ENTRY,
|
|
|
|
|
{
|
|
|
|
|
includeDirs: [PROTO_ROOT , path.join(PROTO_ROOT, 'cmvr/api')], // 关键:让加载器从 PROTO_ROOT 查找 import
|
|
|
|
|
keepCase: true,
|
|
|
|
|
longs: String,
|
|
|
|
|
enums: String,
|
|
|
|
|
defaults: true,
|
|
|
|
|
oneofs: true
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const cmvrProto = grpc.loadPackageDefinition(packageDefinition).cmvr.api
|
|
|
|
|
|
|
|
|
|
const createGrpcClient = (address) => {
|
|
|
|
|
const grpcClient = new cmvrProto.AgvService(
|
|
|
|
|
address,
|
|
|
|
|
grpc.credentials.createInsecure()
|
|
|
|
|
)
|
|
|
|
|
return grpcClient
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取机器人当前地图名称
|
|
|
|
|
app.post('/api/agv/getCurrentMapName', async (req, res) => {
|
|
|
|
|
const request = {
|
|
|
|
|
header: {
|
|
|
|
|
device_id: req.body.device_id || 'agv_src1100'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const grpcClient = createGrpcClient(req.body.ip)
|
|
|
|
|
|
|
|
|
|
const resp = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.getMapStatus(request, (err, res) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 500,
|
|
|
|
|
data: `grpc服务端错误`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 200,
|
|
|
|
|
data: res.status
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (resp.code === 200) {
|
|
|
|
|
res.json(resp)
|
|
|
|
|
} else {
|
|
|
|
|
res.status(500).json({ error: 'grpc服务端错误' })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 获取机器人当前地图名称
|
|
|
|
|
app.post('/api/agv/getCurrentMap', async (req, res) => {
|
|
|
|
|
const grpcClient = createGrpcClient(req.body.ip)
|
|
|
|
|
const request = {
|
|
|
|
|
header: {
|
|
|
|
|
device_id: req.body.device_id || 'agv_src1100'
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
map_name: req.body.mapName
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resp = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.RobotConfigDownloadMap(request, (err, res) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 500,
|
|
|
|
|
data: `grpc服务端错误`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 200,
|
|
|
|
|
data: res.status
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (resp.code === 200) {
|
|
|
|
|
res.json(resp)
|
|
|
|
|
} else {
|
|
|
|
|
res.status(500).json({ error: 'grpc服务端错误' })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.post('/api/agv/getRobotState', async (req, res) => {
|
|
|
|
|
const request = {
|
|
|
|
|
header: {
|
|
|
|
|
device_id: req.body.device_id || 'agv_src1100'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const grpcClient = createGrpcClient(req.body.ip)
|
|
|
|
|
|
|
|
|
|
const response = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.GetRobotLocation(request, (err, res) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 500,
|
|
|
|
|
data: `grpc服务端错误`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 200,
|
|
|
|
|
data: res.status
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
const resp = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.GetBatteryStatus(request, (err, res) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 500,
|
|
|
|
|
data: `grpc服务端错误`
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
resolve({
|
|
|
|
|
code: 200,
|
|
|
|
|
data: res.status
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (response.code === 200 && resp.code ===200) {
|
|
|
|
|
res.json({
|
|
|
|
|
code: 200,
|
|
|
|
|
data: {
|
|
|
|
|
x: response.data.x,
|
|
|
|
|
y: response.data.y,
|
|
|
|
|
angle: response.data.angle,
|
|
|
|
|
confidence: response.data.confidence,
|
|
|
|
|
batteryLevel: resp.data.battery_level,
|
|
|
|
|
batteryTemp: resp.data.battery_temp,
|
|
|
|
|
charging: resp.data.charging,
|
|
|
|
|
voltage: resp.data.voltage
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
res.status(500).json({ error: 'grpc服务端错误' })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.post('/api/agv/moveRobot', async (req, res) => {
|
|
|
|
|
const grpcClient = createGrpcClient(req.body.ip)
|
|
|
|
|
const request = {
|
|
|
|
|
header: {
|
|
|
|
|
device_id: req.body.device_id || 'agv_src1100'
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
vx: req.body.vx,
|
|
|
|
|
vy: req.body.vy,
|
|
|
|
|
w: req.body.w,
|
|
|
|
|
duration: req.body.duration
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resp = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.RobotMotionControl(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 = createGrpcClient(req.body.ip)
|
|
|
|
|
const request = {
|
|
|
|
|
header: {
|
|
|
|
|
device_id: req.body.device_id || 'agv_src1100'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resp = await new Promise((resolve, reject) => {
|
|
|
|
|
grpcClient.RobotControlStop(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服务端错误' })
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-29 11:17:08 +08:00
|
|
|
const isProd = process.env.NODE_ENV === 'production';
|
|
|
|
|
const PORT = process.env.VITE_SERVICE_PORT || 3000;
|
|
|
|
|
|
|
|
|
|
// ============ 生产环境:托管 Vite 构建产物 ============
|
|
|
|
|
if (isProd) {
|
|
|
|
|
const distPath = resolve(__dirname, '../dist');
|
|
|
|
|
|
|
|
|
|
// 托管静态文件
|
|
|
|
|
app.use(express.static(distPath));
|
|
|
|
|
|
|
|
|
|
// SPA 回退:所有非 API 请求返回 index.html
|
|
|
|
|
app.get('*', (req, res) => {
|
|
|
|
|
res.sendFile(resolve(distPath, 'index.html'));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post('*', (req, res) => {
|
|
|
|
|
res.sendFile(resolve(distPath, 'index.html'));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 启动 =========
|
2026-06-26 10:51:01 +08:00
|
|
|
|
|
|
|
|
app.listen(PORT, () => {
|
|
|
|
|
console.log(`gRPC 桥接服务已启动: http://localhost:${PORT}`)
|
|
|
|
|
})
|