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