Files
web-Iot/services/device-access/server.js
2026-08-10 13:33:06 +08:00

177 lines
7.2 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.

/**
* 机器人智慧服务系统 · 设备接入服务 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`);
});