177 lines
7.2 KiB
JavaScript
177 lines
7.2 KiB
JavaScript
/**
|
||
* 机器人智慧服务系统 · 设备接入服务 Alpha (device-access-service)
|
||
* 端口: 3010
|
||
* 职责: 设备注册/注销、心跳保活、在线状态管理、连接凭证签发
|
||
* 数据: services/device-access/data/devices.json(本地文件替代数据库)
|
||
* 文档: 详见 docs/DOC-08-设备数据字典-V1.0草案.md
|
||
*/
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { v4: uuidv4 } = require('uuid');
|
||
|
||
const app = express();
|
||
const PORT = 3010;
|
||
const DATA_FILE = path.join(__dirname, 'data', 'devices.json');
|
||
const HEARTBEAT_TIMEOUT_MS = 60 * 1000; // 心跳超时阈值(60s 未上报判定离线)
|
||
const SCAN_INTERVAL_MS = 30 * 1000; // 离线扫描周期(30s)
|
||
const ONLINE_TTL_MS = 30 * 1000; // 上报后在线有效期(30s,期内视为在线)
|
||
|
||
// 设备类型定义(与 DOC-08 一致)
|
||
const DEVICE_TYPES = ['inspection_robot', 'drone', 'charging_pile', 'gateway', 'sensor'];
|
||
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
|
||
// ─── 文件读写 ─────────────────────────────────────────────
|
||
function readDevices() {
|
||
try {
|
||
const raw = fs.readFileSync(DATA_FILE, 'utf-8');
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function writeDevices(devices) {
|
||
fs.writeFileSync(DATA_FILE, JSON.stringify(devices, null, 2), 'utf-8');
|
||
}
|
||
|
||
// 内存索引(latest heartbeat 不落盘,重启后按 lastHeartbeat 重新判定)
|
||
const onlineCache = new Map(); // deviceId -> lastSeenAt
|
||
|
||
// ─── 工具函数 ─────────────────────────────────────────────
|
||
function toPublic(dev) {
|
||
const lastSeen = onlineCache.get(dev.deviceId);
|
||
const online = !!lastSeen && (Date.now() - lastSeen) < ONLINE_TTL_MS;
|
||
return {
|
||
...dev,
|
||
online,
|
||
lastHeartbeat: lastSeen ? new Date(lastSeen).toISOString() : dev.lastHeartbeat || null,
|
||
};
|
||
}
|
||
|
||
// ─── 路由:注册 ───────────────────────────────────────────
|
||
// POST /api/devices/register 注册设备
|
||
app.post('/api/devices/register', (req, res) => {
|
||
const { deviceId, type, name, secret, capabilities = [], location, meta = {} } = req.body || {};
|
||
if (!deviceId || !type) {
|
||
return res.status(400).json({ code: 40001, msg: 'deviceId 与 type 为必填项' });
|
||
}
|
||
if (!DEVICE_TYPES.includes(type)) {
|
||
return res.status(400).json({ code: 40002, msg: `type 非法,允许值: ${DEVICE_TYPES.join(', ')}` });
|
||
}
|
||
const devices = readDevices();
|
||
if (devices.some((d) => d.deviceId === deviceId)) {
|
||
return res.status(409).json({ code: 40901, msg: `设备 ${deviceId} 已注册` });
|
||
}
|
||
const dev = {
|
||
deviceId,
|
||
type,
|
||
name: name || deviceId,
|
||
secret: secret || uuidv4().replace(/-/g, '').slice(0, 16),
|
||
capabilities,
|
||
location: location || null,
|
||
meta,
|
||
status: 'inactive', // inactive | online | offline
|
||
registeredAt: new Date().toISOString(),
|
||
lastHeartbeat: null,
|
||
};
|
||
devices.push(dev);
|
||
writeDevices(devices);
|
||
onlineCache.set(deviceId, Date.now());
|
||
res.json({ code: 0, msg: '注册成功', data: toPublic(dev) });
|
||
});
|
||
|
||
// ─── 路由:查询 ───────────────────────────────────────────
|
||
// GET /api/devices?type=&online=&keyword= 设备列表
|
||
app.get('/api/devices', (req, res) => {
|
||
const { type, online, keyword } = req.query;
|
||
let list = readDevices().map(toPublic);
|
||
if (type) list = list.filter((d) => d.type === type);
|
||
if (online === 'true') list = list.filter((d) => d.online);
|
||
if (online === 'false') list = list.filter((d) => !d.online);
|
||
if (keyword) list = list.filter((d) => d.deviceId.includes(keyword) || (d.name || '').includes(keyword));
|
||
res.json({ code: 0, data: list });
|
||
});
|
||
|
||
// GET /api/devices/:id 设备详情
|
||
app.get('/api/devices/:id', (req, res) => {
|
||
const dev = readDevices().find((d) => d.deviceId === req.params.id);
|
||
if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
|
||
res.json({ code: 0, data: toPublic(dev) });
|
||
});
|
||
|
||
// GET /api/devices/:id/online 在线状态查询
|
||
app.get('/api/devices/:id/online', (req, res) => {
|
||
const dev = readDevices().find((d) => d.deviceId === req.params.id);
|
||
if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
|
||
res.json({ code: 0, data: { deviceId: dev.deviceId, online: toPublic(dev).online } });
|
||
});
|
||
|
||
// ─── 路由:生命周期 ───────────────────────────────────────
|
||
// POST /api/devices/:id/heartbeat 心跳上报(保活)
|
||
app.post('/api/devices/:id/heartbeat', (req, res) => {
|
||
const devices = readDevices();
|
||
const dev = devices.find((d) => d.deviceId === req.params.id);
|
||
if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
|
||
const now = Date.now();
|
||
onlineCache.set(dev.deviceId, now);
|
||
dev.status = 'online';
|
||
dev.lastHeartbeat = new Date(now).toISOString();
|
||
if (req.body && typeof req.body.meta === 'object') {
|
||
dev.meta = { ...(dev.meta || {}), ...req.body.meta };
|
||
}
|
||
writeDevices(devices);
|
||
res.json({ code: 0, msg: '心跳已记录', data: toPublic(dev) });
|
||
});
|
||
|
||
// POST /api/devices/:id/offline 主动下线
|
||
app.post('/api/devices/:id/offline', (req, res) => {
|
||
const devices = readDevices();
|
||
const dev = devices.find((d) => d.deviceId === req.params.id);
|
||
if (!dev) return res.status(404).json({ code: 40401, msg: '设备不存在' });
|
||
onlineCache.delete(dev.deviceId);
|
||
dev.status = 'offline';
|
||
writeDevices(devices);
|
||
res.json({ code: 0, msg: '设备已下线', data: toPublic(dev) });
|
||
});
|
||
|
||
// DELETE /api/devices/:id 注销设备
|
||
app.delete('/api/devices/:id', (req, res) => {
|
||
const devices = readDevices();
|
||
const idx = devices.findIndex((d) => d.deviceId === req.params.id);
|
||
if (idx === -1) return res.status(404).json({ code: 40401, msg: '设备不存在' });
|
||
const [removed] = devices.splice(idx, 1);
|
||
writeDevices(devices);
|
||
onlineCache.delete(removed.deviceId);
|
||
res.json({ code: 0, msg: '设备已注销', data: { deviceId: removed.deviceId } });
|
||
});
|
||
|
||
// ─── 离线扫描(心跳超时判定)─────────────────────────────
|
||
setInterval(() => {
|
||
const now = Date.now();
|
||
const devices = readDevices();
|
||
let changed = false;
|
||
for (const dev of devices) {
|
||
const lastSeen = onlineCache.get(dev.deviceId);
|
||
if (lastSeen && now - lastSeen > HEARTBEAT_TIMEOUT_MS) {
|
||
onlineCache.delete(dev.deviceId);
|
||
if (dev.status === 'online') {
|
||
dev.status = 'offline';
|
||
changed = true;
|
||
console.log(`[heartbeat-timeout] 设备 ${dev.deviceId} 心跳超时,判定离线`);
|
||
}
|
||
}
|
||
}
|
||
if (changed) writeDevices(devices);
|
||
}, SCAN_INTERVAL_MS);
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`✅ 设备接入服务 Alpha 已启动 → http://localhost:${PORT}`);
|
||
console.log(` API 示例: GET /api/devices`);
|
||
console.log(` POST /api/devices/register {deviceId, type, name, capabilities}`);
|
||
console.log(` POST /api/devices/:id/heartbeat`);
|
||
});
|