126 lines
5.0 KiB
JavaScript
126 lines
5.0 KiB
JavaScript
|
|
/**
|
|||
|
|
* 机器人智慧服务系统 · 状态服务 Alpha (status-service)
|
|||
|
|
* 端口: 3020
|
|||
|
|
* 职责: 设备状态上报接收、最新状态缓存、历史状态查询、SSE 实时订阅
|
|||
|
|
* 数据: services/status/data/status.json(本地文件替代数据库)
|
|||
|
|
* 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md
|
|||
|
|
*/
|
|||
|
|
const express = require('express');
|
|||
|
|
const cors = require('cors');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const app = express();
|
|||
|
|
const PORT = 3020;
|
|||
|
|
const DATA_FILE = path.join(__dirname, 'data', 'status.json');
|
|||
|
|
const MAX_HISTORY_PER_DEVICE = 500; // 每设备历史条数上限(超出丢弃最旧)
|
|||
|
|
|
|||
|
|
app.use(cors());
|
|||
|
|
app.use(express.json());
|
|||
|
|
|
|||
|
|
// ─── 文件读写 ─────────────────────────────────────────────
|
|||
|
|
function readData() {
|
|||
|
|
try {
|
|||
|
|
const raw = fs.readFileSync(DATA_FILE, 'utf-8');
|
|||
|
|
return JSON.parse(raw);
|
|||
|
|
} catch {
|
|||
|
|
return { latest: {}, history: {} };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function writeData(data) {
|
|||
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── SSE 订阅者 ───────────────────────────────────────────
|
|||
|
|
const subscribers = new Set(); // res 集合
|
|||
|
|
|
|||
|
|
function broadcast(payload) {
|
|||
|
|
const msg = `data: ${JSON.stringify(payload)}\n\n`;
|
|||
|
|
for (const res of subscribers) {
|
|||
|
|
try { res.write(msg); } catch { subscribers.delete(res); }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 路由:上报 ───────────────────────────────────────────
|
|||
|
|
// POST /api/status/report 状态上报(设备 -> 平台)
|
|||
|
|
// body: { deviceId, position?, battery?, mode?, speed?, sensors?, alarms?, timestamp? }
|
|||
|
|
app.post('/api/status/report', (req, res) => {
|
|||
|
|
const {
|
|||
|
|
deviceId, position, battery, mode, speed, sensors, alarms, timestamp, meta = {},
|
|||
|
|
} = req.body || {};
|
|||
|
|
if (!deviceId) {
|
|||
|
|
return res.status(400).json({ code: 40001, msg: 'deviceId 为必填项' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const now = new Date().toISOString();
|
|||
|
|
const record = {
|
|||
|
|
deviceId,
|
|||
|
|
position: position || null, // {x,y,z,yaw}
|
|||
|
|
battery: battery ?? null, // 0~100 整数百分比
|
|||
|
|
mode: mode || null, // idle/patrol/charging/working/emergency
|
|||
|
|
speed: speed ?? null, // m/s
|
|||
|
|
sensors: sensors || {}, // 传感器键值
|
|||
|
|
alarms: alarms || [], // 告警编码列表
|
|||
|
|
meta,
|
|||
|
|
ts: timestamp || now, // 设备侧时间(ISO)
|
|||
|
|
receivedAt: now, // 平台接收时间(ISO)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const data = readData();
|
|||
|
|
data.latest[deviceId] = record;
|
|||
|
|
const history = data.history[deviceId] || (data.history[deviceId] = []);
|
|||
|
|
history.push(record);
|
|||
|
|
if (history.length > MAX_HISTORY_PER_DEVICE) history.splice(0, history.length - MAX_HISTORY_PER_DEVICE);
|
|||
|
|
writeData(data);
|
|||
|
|
|
|||
|
|
// 实时广播给订阅者
|
|||
|
|
broadcast({ type: 'status', deviceId, record });
|
|||
|
|
|
|||
|
|
res.json({ code: 0, msg: '状态已接收', data: { deviceId, receivedAt: now } });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ─── 路由:查询 ───────────────────────────────────────────
|
|||
|
|
// GET /api/status/summary 全设备最新状态汇总
|
|||
|
|
app.get('/api/status/summary', (req, res) => {
|
|||
|
|
const data = readData();
|
|||
|
|
res.json({ code: 0, data: Object.values(data.latest) });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// GET /api/status/:deviceId/latest 指定设备最新状态
|
|||
|
|
app.get('/api/status/:deviceId/latest', (req, res) => {
|
|||
|
|
const data = readData();
|
|||
|
|
const record = data.latest[req.params.deviceId];
|
|||
|
|
if (!record) return res.status(404).json({ code: 40401, msg: '该设备暂无状态上报' });
|
|||
|
|
res.json({ code: 0, data: record });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// GET /api/status/history/:deviceId?from=&to=&limit= 历史状态
|
|||
|
|
app.get('/api/status/history/:deviceId', (req, res) => {
|
|||
|
|
const data = readData();
|
|||
|
|
const history = data.history[req.params.deviceId] || [];
|
|||
|
|
const { from, to, limit } = req.query;
|
|||
|
|
let list = history;
|
|||
|
|
if (from) list = list.filter((r) => r.ts >= from);
|
|||
|
|
if (to) list = list.filter((r) => r.ts <= to);
|
|||
|
|
const n = Math.min(parseInt(limit, 10) || 100, 500);
|
|||
|
|
res.json({ code: 0, data: list.slice(-n) });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// GET /api/status/subscribe SSE 实时订阅
|
|||
|
|
app.get('/api/status/subscribe', (req, res) => {
|
|||
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|||
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|||
|
|
res.setHeader('Connection', 'keep-alive');
|
|||
|
|
res.write(`data: ${JSON.stringify({ type: 'connected', msg: '订阅成功' })}\n\n`);
|
|||
|
|
subscribers.add(res);
|
|||
|
|
req.on('close', () => subscribers.delete(res));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
app.listen(PORT, () => {
|
|||
|
|
console.log(`✅ 状态服务 Alpha 已启动 → http://localhost:${PORT}`);
|
|||
|
|
console.log(` API 示例: POST /api/status/report {deviceId, position, battery, mode}`);
|
|||
|
|
console.log(` GET /api/status/:deviceId/latest`);
|
|||
|
|
console.log(` GET /api/status/subscribe (SSE)`);
|
|||
|
|
});
|