@thejrsoft/subway-protocol 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACK_MESSAGES_IMPLEMENTATION_SUMMARY.md +128 -0
- package/ACK_MESSAGE_DESIGN.md +457 -0
- package/CHANGELOG.md +58 -0
- package/COMMAND_VALIDATION_RULES.md +178 -0
- package/DOCUMENTATION_REORGANIZATION_SUMMARY.md +81 -0
- package/DOCUMENTATION_STRUCTURE.md +106 -0
- package/GATEWAY_MIGRATION_GUIDE.md +130 -0
- package/GATEWAY_PROTOCOL_COMPARISON.md +216 -0
- package/INTEGRATION_GUIDE.md +190 -0
- package/OPTIONAL_FIELDS_WITHOUT_DEFAULTS.md +97 -0
- package/PROTOCOL_UTILS_USAGE.md +278 -0
- package/README.md +237 -0
- package/TYPE_FIXES_SUMMARY.md +210 -0
- package/UPDATE_ENUM_VALUES.md +139 -0
- package/dist/asyncapi-sync.d.ts +47 -0
- package/dist/asyncapi-sync.d.ts.map +1 -0
- package/dist/asyncapi-sync.js +85 -0
- package/dist/asyncapi-sync.js.map +1 -0
- package/dist/command-factory.d.ts +62 -0
- package/dist/command-factory.d.ts.map +1 -0
- package/dist/command-factory.js +137 -0
- package/dist/command-factory.js.map +1 -0
- package/dist/command-types.d.ts +27 -0
- package/dist/command-types.d.ts.map +1 -0
- package/dist/command-types.js +31 -0
- package/dist/command-types.js.map +1 -0
- package/dist/index.d.ts +403 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +413 -0
- package/dist/index.js.map +1 -0
- package/dist/message-validator.d.ts +102 -0
- package/dist/message-validator.d.ts.map +1 -0
- package/dist/message-validator.js +640 -0
- package/dist/message-validator.js.map +1 -0
- package/dist/protocol-utils.d.ts +108 -0
- package/dist/protocol-utils.d.ts.map +1 -0
- package/dist/protocol-utils.js +293 -0
- package/dist/protocol-utils.js.map +1 -0
- package/docs/01-protocol/README.md +45 -0
- package/docs/01-protocol/design-rationale.md +198 -0
- package/docs/01-protocol/message-types.md +669 -0
- package/docs/01-protocol/specification.md +1466 -0
- package/docs/02-commands/README.md +56 -0
- package/docs/02-commands/batch-command.md +435 -0
- package/docs/02-commands/complex-command.md +537 -0
- package/docs/02-commands/simple-command.md +332 -0
- package/docs/02-commands/typed-commands.md +362 -0
- package/docs/03-architecture/README.md +66 -0
- package/docs/03-architecture/device-protocol.md +430 -0
- package/docs/03-architecture/edge-proxy.md +727 -0
- package/docs/03-architecture/routing-flow.md +893 -0
- package/docs/04-integration/README.md +144 -0
- package/docs/04-integration/backend-guide.md +551 -0
- package/docs/04-integration/edge-guide.md +684 -0
- package/docs/04-integration/gateway-guide.md +180 -0
- package/docs/04-integration/migration-guide.md +226 -0
- package/docs/05-examples/README.md +141 -0
- package/docs/05-examples/progress-update-examples.md +757 -0
- package/docs/06-reference/README.md +67 -0
- package/docs/06-reference/api.md +572 -0
- package/docs/06-reference/faq.md +302 -0
- package/docs/06-reference/glossary.md +232 -0
- package/examples/backend-upgrade.ts +279 -0
- package/examples/edge-multi-device.ts +513 -0
- package/examples/gateway-upgrade.ts +150 -0
- package/examples/protocol-implementation.ts +715 -0
- package/package.json +48 -0
- package/scripts/validate-asyncapi.ts +78 -0
- package/src/__tests__/protocol.test.ts +297 -0
- package/src/asyncapi-sync.ts +84 -0
- package/src/command-factory.ts +183 -0
- package/src/command-types.ts +72 -0
- package/src/edge-proxy.ts +494 -0
- package/src/gateway-extensions.ts +278 -0
- package/src/index.ts +792 -0
- package/src/message-validator.ts +726 -0
- package/src/protocol-utils.ts +355 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
# Edge 代理模式指南
|
|
2
|
+
|
|
3
|
+
本指南说明 Edge 如何作为设备代理与 Gateway 通信。
|
|
4
|
+
|
|
5
|
+
## 架构概述
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
设备端层 Edge 层 Gateway 层 Backend 层
|
|
9
|
+
(隧道媒体系统)
|
|
10
|
+
┌─────────┐ WS ┌─────────────┐ WS ┌──────────────┐ ┌──────────┐
|
|
11
|
+
│ TD-01 ├─────────┤ ├──────────┤ ├───────┤ │
|
|
12
|
+
│ TD-02 ├─────────┤ Edge A │ │ │ │ Backend │
|
|
13
|
+
│ TD-03 ├─────────┤ │ │ │ │ │
|
|
14
|
+
└─────────┘ └─────────────┘ │ │ └──────────┘
|
|
15
|
+
↕ │ Gateway │
|
|
16
|
+
┌─────────┐ WS ┌─────────────┐ WS │ │
|
|
17
|
+
│ TD-04 ├─────────┤ ├──────────┤ │
|
|
18
|
+
│ TD-05 ├─────────┤ Edge B │ │ │
|
|
19
|
+
└─────────┘ └─────────────┘ └──────────────┘
|
|
20
|
+
|
|
21
|
+
WS = WebSocket 连接
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Edge 的职责
|
|
25
|
+
|
|
26
|
+
1. **设备管理** - 管理隧道媒体广告播放系统的设备端
|
|
27
|
+
2. **WebSocket 服务** - 为设备端提供 WebSocket 连接服务
|
|
28
|
+
3. **心跳维护** - 维护与设备端的心跳(30秒间隔,90秒超时)
|
|
29
|
+
4. **命令路由** - 将 Gateway 命令路由到正确的设备端
|
|
30
|
+
5. **状态聚合** - 收集并上报设备状态
|
|
31
|
+
6. **离线缓存** - 在网络中断时缓存命令和数据
|
|
32
|
+
7. **设备注册代理** - 为连接的设备向 Gateway 注册
|
|
33
|
+
|
|
34
|
+
## 实现 Edge 代理
|
|
35
|
+
|
|
36
|
+
### 1. Edge 初始化和注册
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
EdgeProxy,
|
|
41
|
+
EdgeProxyUtils,
|
|
42
|
+
ManagedDevice
|
|
43
|
+
} from '@jrsoft/subway-protocol';
|
|
44
|
+
|
|
45
|
+
class MyEdgeProxy extends EdgeProxy {
|
|
46
|
+
constructor() {
|
|
47
|
+
super('edge-001'); // Edge ID
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async initialize() {
|
|
51
|
+
// 1. 启动 WebSocket 服务器,接受设备端连接
|
|
52
|
+
await this.startDeviceServer(8080);
|
|
53
|
+
|
|
54
|
+
// 2. 连接到 Gateway
|
|
55
|
+
await this.connectToGateway('ws://gateway:18081');
|
|
56
|
+
|
|
57
|
+
// 3. 注册 Edge 自身
|
|
58
|
+
await this.registerToGateway();
|
|
59
|
+
|
|
60
|
+
// 4. 为已连接的设备端注册(场景1:Edge 先启动)
|
|
61
|
+
for (const [deviceId, device] of this.connectedDevices) {
|
|
62
|
+
await this.registerDeviceToGateway(deviceId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 处理设备端 WebSocket 连接
|
|
67
|
+
private async handleDeviceConnection(ws: WebSocket, deviceId: string) {
|
|
68
|
+
// 设备端注册消息
|
|
69
|
+
ws.on('message', async (data) => {
|
|
70
|
+
const msg = JSON.parse(data.toString());
|
|
71
|
+
|
|
72
|
+
if (msg.type === 'register') {
|
|
73
|
+
// 记录设备信息
|
|
74
|
+
this.connectedDevices.set(msg.clientId, {
|
|
75
|
+
ws,
|
|
76
|
+
deviceId: msg.clientId,
|
|
77
|
+
deviceType: msg.clientInfo?.deviceType || 'media_player',
|
|
78
|
+
capabilities: msg.clientInfo?.capabilities || [],
|
|
79
|
+
status: 'online',
|
|
80
|
+
lastSeen: new Date().toISOString()
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 向设备端确认注册
|
|
84
|
+
ws.send(JSON.stringify({
|
|
85
|
+
type: 'register_ack',
|
|
86
|
+
clientId: msg.clientId,
|
|
87
|
+
success: true,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
version: '1.0'
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// 场景2:Gateway 在线,立即注册新设备
|
|
93
|
+
if (this.isConnectedToGateway()) {
|
|
94
|
+
await this.registerDeviceToGateway(msg.clientId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (msg.type === 'heartbeat') {
|
|
99
|
+
// 响应心跳
|
|
100
|
+
ws.send(JSON.stringify({
|
|
101
|
+
type: 'heartbeat_ack',
|
|
102
|
+
clientId: msg.clientId,
|
|
103
|
+
sequence: msg.sequence,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
version: '1.0'
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
// 更新最后活跃时间
|
|
109
|
+
const device = this.connectedDevices.get(msg.clientId);
|
|
110
|
+
if (device) {
|
|
111
|
+
device.lastSeen = new Date().toISOString();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. 注册流程
|
|
120
|
+
|
|
121
|
+
#### 2.1 Edge 自身注册
|
|
122
|
+
|
|
123
|
+
Edge 先注册自己到 Gateway:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"type": "REGISTER",
|
|
128
|
+
"clientId": "edge-001",
|
|
129
|
+
"clientType": "EDGE",
|
|
130
|
+
"clientInfo": {
|
|
131
|
+
"version": "1.0.0",
|
|
132
|
+
"platform": "edge-proxy",
|
|
133
|
+
"capabilities": ["device_proxy", "batch_command", "status_report"]
|
|
134
|
+
},
|
|
135
|
+
"timestamp": "2024-01-20T10:00:00Z",
|
|
136
|
+
"version": "1.0"
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 2.2 设备端通过 Edge 注册
|
|
141
|
+
|
|
142
|
+
Edge 为每个连接的设备端发送注册消息到 Gateway:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"type": "REGISTER",
|
|
147
|
+
"clientId": "td-01", // 设备端 ID
|
|
148
|
+
"clientType": "DEVICE",
|
|
149
|
+
"edgeInfo": { // Edge 信息
|
|
150
|
+
"edgeId": "edge-001",
|
|
151
|
+
"edgeVersion": "1.0.0",
|
|
152
|
+
"connectionTime": "2024-01-20T09:59:00Z"
|
|
153
|
+
},
|
|
154
|
+
"clientInfo": {
|
|
155
|
+
"version": "1.0.0",
|
|
156
|
+
"deviceType": "media_player",
|
|
157
|
+
"capabilities": ["play", "stop", "status_report", "program_upload"]
|
|
158
|
+
},
|
|
159
|
+
"timestamp": "2024-01-20T10:00:01Z",
|
|
160
|
+
"version": "1.0"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 3. 设备寻址
|
|
165
|
+
|
|
166
|
+
Backend 直接使用设备ID,Gateway 根据路由表自动找到对应的 Edge:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Backend 发送命令,只需指定设备ID
|
|
170
|
+
const command = MessageFactory.createCommandMessage(
|
|
171
|
+
'cmd-123',
|
|
172
|
+
'td-01', // 目标设备ID,Gateway会自动路由到管理该设备的Edge
|
|
173
|
+
{
|
|
174
|
+
commandType: CommandType.SIMPLE,
|
|
175
|
+
commandCode: 'ProgramUpload',
|
|
176
|
+
deviceType: 'media_player',
|
|
177
|
+
deviceId: 1,
|
|
178
|
+
operationType: 'write',
|
|
179
|
+
parameters: {
|
|
180
|
+
taskId: '1234567890123456789',
|
|
181
|
+
programId: '1234567890123456788',
|
|
182
|
+
programName: '春节活动广告',
|
|
183
|
+
// ... 其他参数
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 4. 命令处理流程
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
class MyEdgeProxy extends EdgeProxy {
|
|
193
|
+
// 处理来自 Gateway 的命令
|
|
194
|
+
protected async handleGatewayCommand(message: CommandMessage) {
|
|
195
|
+
// targetClientId 直接就是设备ID
|
|
196
|
+
const deviceId = message.targetClientId;
|
|
197
|
+
|
|
198
|
+
const device = this.connectedDevices.get(deviceId);
|
|
199
|
+
if (!device) {
|
|
200
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 转发命令到设备端(通过 WebSocket)
|
|
204
|
+
// 直接转发 command 消息,保持消息类型不变
|
|
205
|
+
device.ws.send(JSON.stringify(message));
|
|
206
|
+
|
|
207
|
+
// 等待设备响应
|
|
208
|
+
return await this.waitForDeviceResponse(message.requestRef, message.timeout);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 处理 Complex 类型命令的持续响应
|
|
212
|
+
private async handleComplexCommand(device: any, command: CommandMessage) {
|
|
213
|
+
// 直接转发命令
|
|
214
|
+
device.ws.send(JSON.stringify(command));
|
|
215
|
+
|
|
216
|
+
// Complex 命令会收到多个响应
|
|
217
|
+
device.ws.on('message', (data) => {
|
|
218
|
+
const response = JSON.parse(data.toString());
|
|
219
|
+
|
|
220
|
+
if (response.requestRef === command.requestRef) {
|
|
221
|
+
// 转发进度更新到 Gateway
|
|
222
|
+
if (response.type === 'progress_update') {
|
|
223
|
+
this.forwardToGateway(response);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 最终响应
|
|
227
|
+
if (response.type === 'command_response') {
|
|
228
|
+
this.forwardToGateway(response);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 5. 批量命令支持
|
|
237
|
+
|
|
238
|
+
批量命令是向同一设备端的多个同类型子硬件发送相同命令:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
// Backend 发送批量命令到光柱
|
|
242
|
+
const batchCommand = MessageFactory.createCommandMessage(
|
|
243
|
+
'batch-001',
|
|
244
|
+
'td-01', // 目标设备ID
|
|
245
|
+
{
|
|
246
|
+
commandType: CommandType.BATCH,
|
|
247
|
+
commandCode: 'TRAIN_LENGTH',
|
|
248
|
+
deviceType: 'pillar',
|
|
249
|
+
deviceId: '1-10,30-40,51,52,53,54', // 批量目标:范围表示法
|
|
250
|
+
operationType: 'write',
|
|
251
|
+
parameters: {
|
|
252
|
+
switch: 'ON'
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
priority: Priority.HIGH,
|
|
257
|
+
timeout: 10000
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Edge 处理批量命令
|
|
262
|
+
class MyEdgeProxy extends EdgeProxy {
|
|
263
|
+
private async handleBatchCommand(device: any, command: CommandMessage) {
|
|
264
|
+
// 解析批量目标
|
|
265
|
+
const targets = this.parseTargetRange(command.command.deviceId);
|
|
266
|
+
|
|
267
|
+
// 转发到设备端,由设备端执行批量操作
|
|
268
|
+
// 将解析后的目标列表添加到命令参数中
|
|
269
|
+
const deviceCommand = {
|
|
270
|
+
...command,
|
|
271
|
+
command: {
|
|
272
|
+
...command.command,
|
|
273
|
+
parameters: {
|
|
274
|
+
...command.command.parameters,
|
|
275
|
+
targets // 添加解析后的目标列表
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
device.ws.send(JSON.stringify(deviceCommand));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 解析范围表示法
|
|
284
|
+
private parseTargetRange(rangeStr: string): number[] {
|
|
285
|
+
if (rangeStr === '0') return [0]; // 0 表示所有
|
|
286
|
+
|
|
287
|
+
const targets: number[] = [];
|
|
288
|
+
const parts = rangeStr.split(',');
|
|
289
|
+
|
|
290
|
+
for (const part of parts) {
|
|
291
|
+
if (part.includes('-')) {
|
|
292
|
+
const [start, end] = part.split('-').map(Number);
|
|
293
|
+
for (let i = start; i <= end; i++) {
|
|
294
|
+
targets.push(i);
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
targets.push(Number(part));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return targets;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 6. 设备状态监控和上报
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
class MyEdgeProxy extends EdgeProxy {
|
|
310
|
+
private heartbeatTimeouts = new Map<string, NodeJS.Timeout>();
|
|
311
|
+
|
|
312
|
+
startMonitoring() {
|
|
313
|
+
// 监控设备端心跳(90秒超时)
|
|
314
|
+
this.monitorDeviceHeartbeats();
|
|
315
|
+
|
|
316
|
+
// 定期向 Gateway 发送心跳
|
|
317
|
+
setInterval(() => {
|
|
318
|
+
this.sendHeartbeatToGateway();
|
|
319
|
+
}, 30000); // 30秒
|
|
320
|
+
|
|
321
|
+
// 定期发送设备状态汇总报告
|
|
322
|
+
setInterval(() => {
|
|
323
|
+
this.sendStatusReport();
|
|
324
|
+
}, 60000); // 60秒
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private monitorDeviceHeartbeats() {
|
|
328
|
+
// 每次收到设备心跳时重置超时计时器
|
|
329
|
+
this.on('device_heartbeat', (deviceId: string) => {
|
|
330
|
+
// 清除旧的超时计时器
|
|
331
|
+
const oldTimeout = this.heartbeatTimeouts.get(deviceId);
|
|
332
|
+
if (oldTimeout) {
|
|
333
|
+
clearTimeout(oldTimeout);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 设置新的90秒超时计时器
|
|
337
|
+
const timeout = setTimeout(() => {
|
|
338
|
+
this.handleDeviceTimeout(deviceId);
|
|
339
|
+
}, 90000);
|
|
340
|
+
|
|
341
|
+
this.heartbeatTimeouts.set(deviceId, timeout);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private handleDeviceTimeout(deviceId: string) {
|
|
346
|
+
const device = this.connectedDevices.get(deviceId);
|
|
347
|
+
if (device && device.status === 'online') {
|
|
348
|
+
// 标记设备离线
|
|
349
|
+
device.status = 'offline';
|
|
350
|
+
|
|
351
|
+
// 通知 Gateway 设备离线
|
|
352
|
+
const notification = {
|
|
353
|
+
type: 'device_status_change',
|
|
354
|
+
edgeId: this.edgeId,
|
|
355
|
+
deviceId: deviceId,
|
|
356
|
+
previousStatus: 'online',
|
|
357
|
+
currentStatus: 'offline',
|
|
358
|
+
reason: 'heartbeat_timeout',
|
|
359
|
+
timestamp: new Date().toISOString(),
|
|
360
|
+
version: '1.0'
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
this.sendToGateway(notification);
|
|
364
|
+
|
|
365
|
+
// 尝试关闭 WebSocket 连接
|
|
366
|
+
if (device.ws) {
|
|
367
|
+
device.ws.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private sendStatusReport() {
|
|
373
|
+
const report = {
|
|
374
|
+
type: 'edge_status_report',
|
|
375
|
+
edgeId: this.edgeId,
|
|
376
|
+
timestamp: new Date().toISOString(),
|
|
377
|
+
deviceCount: this.connectedDevices.size,
|
|
378
|
+
devices: Array.from(this.connectedDevices.entries()).map(([id, device]) => ({
|
|
379
|
+
deviceId: id,
|
|
380
|
+
status: device.status,
|
|
381
|
+
lastSeen: device.lastSeen,
|
|
382
|
+
deviceType: device.deviceType
|
|
383
|
+
})),
|
|
384
|
+
metrics: {
|
|
385
|
+
cpuUsage: process.cpuUsage(),
|
|
386
|
+
memoryUsage: process.memoryUsage(),
|
|
387
|
+
uptime: process.uptime()
|
|
388
|
+
},
|
|
389
|
+
version: '1.0'
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
this.sendToGateway(report);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 7. 进度更新处理
|
|
398
|
+
|
|
399
|
+
Edge 需要正确处理和转发设备的进度更新消息:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
class MyEdgeProxy extends EdgeProxy {
|
|
403
|
+
// 处理设备端的进度更新
|
|
404
|
+
private handleDeviceProgressUpdate(deviceId: string, update: ProgressUpdateMessage) {
|
|
405
|
+
// 记录日志
|
|
406
|
+
if (update.log) {
|
|
407
|
+
this.logDeviceMessage(deviceId, update.log.level, update.log.code, update.log.data);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 检查状态
|
|
411
|
+
switch (update.status) {
|
|
412
|
+
case 'failed':
|
|
413
|
+
this.handleDeviceError(deviceId, update);
|
|
414
|
+
break;
|
|
415
|
+
case 'paused':
|
|
416
|
+
this.handleDevicePaused(deviceId, update);
|
|
417
|
+
break;
|
|
418
|
+
case 'cancelled':
|
|
419
|
+
this.handleDeviceCancelled(deviceId, update);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 转发到 Gateway
|
|
424
|
+
this.forwardToGateway(update);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 处理设备错误
|
|
428
|
+
private handleDeviceError(deviceId: string, update: ProgressUpdateMessage) {
|
|
429
|
+
if (update.log?.level === 'critical') {
|
|
430
|
+
// 严重错误,可能需要重启设备
|
|
431
|
+
this.scheduleDeviceRestart(deviceId);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 记录错误统计
|
|
435
|
+
this.errorStats.record(deviceId, update.log?.code || 'UNKNOWN_ERROR');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 汇总进度信息
|
|
439
|
+
private aggregateProgress(deviceId: string, update: ProgressUpdateMessage) {
|
|
440
|
+
const progress = this.deviceProgress.get(deviceId) || {};
|
|
441
|
+
progress[update.requestRef] = {
|
|
442
|
+
phase: update.phase,
|
|
443
|
+
progress: update.progress,
|
|
444
|
+
status: update.status,
|
|
445
|
+
lastUpdate: update.timestamp
|
|
446
|
+
};
|
|
447
|
+
this.deviceProgress.set(deviceId, progress);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 8. 离线模式和数据缓存
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
class OfflineCapableEdge extends EdgeProxy {
|
|
456
|
+
private commandQueue: CommandMessage[] = [];
|
|
457
|
+
private dataCache: Map<string, any> = new Map();
|
|
458
|
+
|
|
459
|
+
protected async handleGatewayCommand(message: CommandMessage) {
|
|
460
|
+
if (!this.isConnectedToGateway()) {
|
|
461
|
+
// 离线模式:缓存命令
|
|
462
|
+
this.commandQueue.push(message);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await super.handleGatewayCommand(message);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
// 执行失败,根据策略决定是否缓存
|
|
470
|
+
if (this.shouldCacheOnError(error)) {
|
|
471
|
+
this.commandQueue.push(message);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 重连后处理缓存的命令
|
|
477
|
+
private async processCachedCommands() {
|
|
478
|
+
while (this.commandQueue.length > 0) {
|
|
479
|
+
const command = this.commandQueue.shift()!;
|
|
480
|
+
try {
|
|
481
|
+
await super.handleGatewayCommand(command);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error('Failed to process cached command:', error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 缓存设备数据供离线查询
|
|
489
|
+
private cacheDeviceData(deviceId: string, data: any) {
|
|
490
|
+
this.dataCache.set(
|
|
491
|
+
`${deviceId}:${Date.now()}`,
|
|
492
|
+
{
|
|
493
|
+
deviceId,
|
|
494
|
+
data,
|
|
495
|
+
timestamp: new Date().toISOString()
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## 最佳实践
|
|
503
|
+
|
|
504
|
+
### 1. 设备分组管理
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
// 按类型、位置或功能分组设备
|
|
508
|
+
const deviceGroups = {
|
|
509
|
+
floor1: ['plc-01', 'plc-02', 'sensor-01'],
|
|
510
|
+
floor2: ['plc-03', 'plc-04', 'sensor-02'],
|
|
511
|
+
critical: ['plc-01', 'plc-03'] // 关键设备
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// 支持按组执行命令
|
|
515
|
+
async function executeGroupCommand(group: string, command: any) {
|
|
516
|
+
const devices = deviceGroups[group] || [];
|
|
517
|
+
return await EdgeProxyUtils.createEdgeBatchCommand(
|
|
518
|
+
`group-${group}`,
|
|
519
|
+
'edge-01',
|
|
520
|
+
devices,
|
|
521
|
+
command
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 2. 错误处理和恢复
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
class ResilientEdge extends EdgeProxy {
|
|
530
|
+
private deviceRetryCount = new Map<string, number>();
|
|
531
|
+
|
|
532
|
+
protected async handleGatewayCommand(message: CommandMessage): Promise<any> {
|
|
533
|
+
// targetClientId 直接就是设备ID
|
|
534
|
+
const deviceId = message.targetClientId;
|
|
535
|
+
const device = this.connectedDevices.get(deviceId);
|
|
536
|
+
|
|
537
|
+
if (!device) {
|
|
538
|
+
throw new Error(`Device ${deviceId} not found`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const maxRetries = 3;
|
|
542
|
+
let lastError;
|
|
543
|
+
|
|
544
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
545
|
+
try {
|
|
546
|
+
// 发送命令到设备
|
|
547
|
+
device.ws.send(JSON.stringify(message));
|
|
548
|
+
|
|
549
|
+
// 等待响应
|
|
550
|
+
const result = await this.waitForDeviceResponse(message.requestRef, message.timeout);
|
|
551
|
+
|
|
552
|
+
// 成功,重置重试计数
|
|
553
|
+
this.deviceRetryCount.set(deviceId, 0);
|
|
554
|
+
return result;
|
|
555
|
+
|
|
556
|
+
} catch (error) {
|
|
557
|
+
lastError = error;
|
|
558
|
+
const retryCount = (this.deviceRetryCount.get(deviceId) || 0) + 1;
|
|
559
|
+
this.deviceRetryCount.set(deviceId, retryCount);
|
|
560
|
+
|
|
561
|
+
// 指数退避
|
|
562
|
+
if (i < maxRetries - 1) {
|
|
563
|
+
await this.delay(Math.pow(2, i) * 1000);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 标记设备为错误状态
|
|
569
|
+
device.status = 'error';
|
|
570
|
+
this.notifyDeviceError(deviceId, lastError);
|
|
571
|
+
|
|
572
|
+
throw lastError;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private async notifyDeviceError(deviceId: string, error: any) {
|
|
576
|
+
const errorNotification = {
|
|
577
|
+
type: 'error',
|
|
578
|
+
code: 'DEVICE_ERROR',
|
|
579
|
+
message: `Device ${deviceId} error: ${error.message}`,
|
|
580
|
+
severity: 'high',
|
|
581
|
+
category: 'device',
|
|
582
|
+
context: {
|
|
583
|
+
edgeId: this.edgeId,
|
|
584
|
+
deviceId: deviceId,
|
|
585
|
+
error: error.toString()
|
|
586
|
+
},
|
|
587
|
+
retryable: true,
|
|
588
|
+
timestamp: new Date().toISOString(),
|
|
589
|
+
version: '1.0'
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
this.sendToGateway(errorNotification);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### 3. 性能优化
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// 1. 命令合并
|
|
601
|
+
class OptimizedEdge extends EdgeProxy {
|
|
602
|
+
private pendingCommands = new Map<string, CommandMessage[]>();
|
|
603
|
+
|
|
604
|
+
// 合并相同设备的读命令
|
|
605
|
+
protected optimizeReadCommands(commands: CommandMessage[]): CommandMessage[] {
|
|
606
|
+
const grouped = this.groupByDevice(commands);
|
|
607
|
+
|
|
608
|
+
return grouped.map(group => {
|
|
609
|
+
if (group.length === 1) return group[0];
|
|
610
|
+
|
|
611
|
+
// 合并多个读取请求
|
|
612
|
+
return this.mergeReadCommands(group);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 2. 数据预取
|
|
618
|
+
class PrefetchingEdge extends EdgeProxy {
|
|
619
|
+
private prefetchPatterns = new Map<string, string[]>();
|
|
620
|
+
|
|
621
|
+
// 根据历史模式预取数据
|
|
622
|
+
protected async handleGatewayCommand(message: CommandMessage) {
|
|
623
|
+
await super.handleGatewayCommand(message);
|
|
624
|
+
|
|
625
|
+
// 分析命令模式
|
|
626
|
+
this.analyzeCommandPattern(message);
|
|
627
|
+
|
|
628
|
+
// 预取相关数据
|
|
629
|
+
const relatedData = this.prefetchPatterns.get(message.command.commandCode);
|
|
630
|
+
if (relatedData) {
|
|
631
|
+
this.prefetchDeviceData(message.clientId, relatedData);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### 4. 监控和告警
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
interface EdgeAlert {
|
|
641
|
+
level: 'info' | 'warning' | 'error' | 'critical';
|
|
642
|
+
source: string;
|
|
643
|
+
message: string;
|
|
644
|
+
timestamp: string;
|
|
645
|
+
deviceId?: string;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
class MonitoredEdge extends EdgeProxy {
|
|
649
|
+
private alerts: EdgeAlert[] = [];
|
|
650
|
+
|
|
651
|
+
protected sendAlert(alert: EdgeAlert) {
|
|
652
|
+
this.alerts.push(alert);
|
|
653
|
+
|
|
654
|
+
// 发送到 Gateway
|
|
655
|
+
this.sendToGateway({
|
|
656
|
+
type: 'edge_alert',
|
|
657
|
+
edgeId: this.edgeId,
|
|
658
|
+
alert
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// 严重告警立即通知
|
|
662
|
+
if (alert.level === 'critical') {
|
|
663
|
+
this.notifyCriticalAlert(alert);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 监控指标
|
|
668
|
+
getMetrics() {
|
|
669
|
+
return {
|
|
670
|
+
deviceCount: this.devices.size,
|
|
671
|
+
onlineDevices: Array.from(this.devices.values())
|
|
672
|
+
.filter(d => d.status === 'online').length,
|
|
673
|
+
commandsPerMinute: this.calculateCommandRate(),
|
|
674
|
+
errorRate: this.calculateErrorRate(),
|
|
675
|
+
avgResponseTime: this.calculateAvgResponseTime()
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## 故障场景处理
|
|
682
|
+
|
|
683
|
+
### 1. Gateway 连接中断
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// Edge 自动切换到离线模式
|
|
687
|
+
// 缓存数据和命令
|
|
688
|
+
// 保持设备正常运行
|
|
689
|
+
// 重连后同步状态
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### 2. 设备故障
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
// 标记设备离线
|
|
696
|
+
// 通知 Gateway
|
|
697
|
+
// 尝试恢复
|
|
698
|
+
// 提供故障诊断信息
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 3. 批量命令部分失败
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// 继续执行其他设备命令
|
|
705
|
+
// 收集所有结果
|
|
706
|
+
// 返回详细的执行报告
|
|
707
|
+
// 支持重试失败的命令
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## 总结
|
|
711
|
+
|
|
712
|
+
Edge 代理模式在 JRSoft Subway 系统中的作用:
|
|
713
|
+
|
|
714
|
+
1. **WebSocket 代理** - 为设备端提供 WebSocket 服务,管理隧道媒体广告播放系统
|
|
715
|
+
2. **双向注册** - Edge 自身注册 + 代理设备端注册
|
|
716
|
+
3. **心跳维护** - 设备端→Edge(30秒/90秒),Edge→Gateway(30秒/90秒)
|
|
717
|
+
4. **命令路由** - 支持 Simple、Batch、Complex 三种命令类型
|
|
718
|
+
5. **状态监控** - 实时监控设备状态,及时上报异常
|
|
719
|
+
6. **离线缓存** - Gateway 断线时缓存数据,重连后同步
|
|
720
|
+
|
|
721
|
+
关键特性:
|
|
722
|
+
- 使用 `edgeId:deviceId` 格式进行设备寻址
|
|
723
|
+
- 支持批量命令的范围表示法(如 "1-10,30-40,51")
|
|
724
|
+
- Complex 命令支持持续响应(如健康检查)
|
|
725
|
+
- 所有消息使用协议版本 "1.0"
|
|
726
|
+
|
|
727
|
+
通过 Edge 代理,可以将设备管理逻辑下沉到边缘,提高系统的可扩展性和可靠性。
|