roslib-ts 0.1.1 → 1.0.0-beta.1
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/API.md +293 -0
- package/LICENSE +21 -0
- package/README.md +63 -21
- package/README.zh-CN.md +96 -0
- package/dist/index.cjs.js +662 -70
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +212 -8
- package/dist/index.esm.js +659 -71
- package/dist/index.esm.js.map +1 -1
- package/dist/next.cjs.js +22 -0
- package/dist/next.cjs.js.map +1 -0
- package/dist/next.d.ts +1 -0
- package/dist/next.esm.js +2 -0
- package/dist/next.esm.js.map +1 -0
- package/dist/types/EnhancedRos.d.ts +131 -0
- package/dist/types/EnhancedRos.d.ts.map +1 -0
- package/dist/types/Param.d.ts +2 -2
- package/dist/types/Param.d.ts.map +1 -1
- package/dist/types/Ros.d.ts +10 -2
- package/dist/types/Ros.d.ts.map +1 -1
- package/dist/types/RosManagers.d.ts +40 -0
- package/dist/types/RosManagers.d.ts.map +1 -0
- package/dist/types/Service.d.ts +18 -3
- package/dist/types/Service.d.ts.map +1 -1
- package/dist/types/Topic.d.ts +19 -3
- package/dist/types/Topic.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/next.d.ts +9 -0
- package/dist/types/next.d.ts.map +1 -0
- package/package.json +18 -11
package/dist/index.esm.js
CHANGED
|
@@ -59,13 +59,11 @@ class EventEmitter {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
class Ros extends EventEmitter {
|
|
62
|
-
// private reconnectDelay = 1000;
|
|
63
62
|
constructor(options = {}) {
|
|
64
63
|
super();
|
|
65
64
|
this.socket = null;
|
|
66
65
|
this._isConnected = false;
|
|
67
66
|
this.idCounter = 0;
|
|
68
|
-
this.reconnectTimer = null;
|
|
69
67
|
this.options = options;
|
|
70
68
|
if (options.url) {
|
|
71
69
|
this.connect(options.url);
|
|
@@ -88,11 +86,6 @@ class Ros extends EventEmitter {
|
|
|
88
86
|
this.socket.onopen = () => {
|
|
89
87
|
this._isConnected = true;
|
|
90
88
|
this.emit('connection');
|
|
91
|
-
// 清除重连定时器
|
|
92
|
-
if (this.reconnectTimer) {
|
|
93
|
-
clearTimeout(this.reconnectTimer);
|
|
94
|
-
this.reconnectTimer = null;
|
|
95
|
-
}
|
|
96
89
|
};
|
|
97
90
|
this.socket.onclose = () => {
|
|
98
91
|
this._isConnected = false;
|
|
@@ -115,10 +108,6 @@ class Ros extends EventEmitter {
|
|
|
115
108
|
this.socket = null;
|
|
116
109
|
}
|
|
117
110
|
this._isConnected = false;
|
|
118
|
-
if (this.reconnectTimer) {
|
|
119
|
-
clearTimeout(this.reconnectTimer);
|
|
120
|
-
this.reconnectTimer = null;
|
|
121
|
-
}
|
|
122
111
|
}
|
|
123
112
|
handleMessage(data) {
|
|
124
113
|
try {
|
|
@@ -164,11 +153,361 @@ class Ros extends EventEmitter {
|
|
|
164
153
|
}
|
|
165
154
|
}
|
|
166
155
|
|
|
156
|
+
/**
|
|
157
|
+
* 事件发射器,用于内部实现
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* 连接状态枚举
|
|
161
|
+
*/
|
|
162
|
+
var EnhancedRosState;
|
|
163
|
+
(function (EnhancedRosState) {
|
|
164
|
+
/** 空闲/初始状态 */
|
|
165
|
+
EnhancedRosState["IDLE"] = "IDLE";
|
|
166
|
+
/** 正在连接 */
|
|
167
|
+
EnhancedRosState["CONNECTING"] = "CONNECTING";
|
|
168
|
+
/** 已连接 */
|
|
169
|
+
EnhancedRosState["CONNECTED"] = "CONNECTED";
|
|
170
|
+
/** 正在重连 */
|
|
171
|
+
EnhancedRosState["RECONNECTING"] = "RECONNECTING";
|
|
172
|
+
/** 已手动关闭 */
|
|
173
|
+
EnhancedRosState["CLOSED"] = "CLOSED";
|
|
174
|
+
/** 发生错误 */
|
|
175
|
+
EnhancedRosState["ERROR"] = "ERROR";
|
|
176
|
+
})(EnhancedRosState || (EnhancedRosState = {}));
|
|
177
|
+
/**
|
|
178
|
+
* 增强版 ROS 连接封装
|
|
179
|
+
* 支持自动重连、心跳保活、消息队列、状态管理
|
|
180
|
+
*/
|
|
181
|
+
class EnhancedRos extends EventEmitter {
|
|
182
|
+
/**
|
|
183
|
+
* 构造函数
|
|
184
|
+
* @param options 配置项
|
|
185
|
+
*/
|
|
186
|
+
constructor(options = {}) {
|
|
187
|
+
var _a, _b, _c;
|
|
188
|
+
super();
|
|
189
|
+
/** WebSocket 实例 */
|
|
190
|
+
this.socket = null;
|
|
191
|
+
/** 自增 ID 计数器,用于请求-响应匹配 */
|
|
192
|
+
this.idCounter = 0;
|
|
193
|
+
/** 当前连接状态 */
|
|
194
|
+
this._state = EnhancedRosState.IDLE;
|
|
195
|
+
/** 当前/最近一次连接地址 */
|
|
196
|
+
this.currentUrl = null;
|
|
197
|
+
/** 离线消息队列,连接成功后自动发送 */
|
|
198
|
+
this.messageQueue = [];
|
|
199
|
+
/** 重连定时器句柄 */
|
|
200
|
+
this.reconnectTimer = null;
|
|
201
|
+
/** 心跳定时器句柄 */
|
|
202
|
+
this.heartbeatTimer = null;
|
|
203
|
+
/** 最近一次收到服务端消息的时间戳 */
|
|
204
|
+
this.lastServerMessageAtMs = null;
|
|
205
|
+
/** 标记是否为用户主动关闭(影响重连策略) */
|
|
206
|
+
this.manualClose = false;
|
|
207
|
+
/** 连接代际,用于丢弃过期的重连任务 */
|
|
208
|
+
this.connectGeneration = 0;
|
|
209
|
+
this.options = options;
|
|
210
|
+
// 初始化重连退避参数
|
|
211
|
+
this.reconnectMinDelayMs = (_a = options.reconnect_min_delay) !== null && _a !== void 0 ? _a : 1000;
|
|
212
|
+
this.reconnectMaxDelayMs = (_b = options.reconnect_max_delay) !== null && _b !== void 0 ? _b : 30000;
|
|
213
|
+
this.reconnectDelayMs = this.reconnectMinDelayMs;
|
|
214
|
+
// 初始化心跳参数 开启的话12000ms,不开启0ms
|
|
215
|
+
this.heartbeatIntervalMs = (_c = options.heartbeat_interval_ms) !== null && _c !== void 0 ? _c : 0;
|
|
216
|
+
this.heartbeatFn = options.heartbeat_fn;
|
|
217
|
+
// 如果提供了 url,立即开始连接
|
|
218
|
+
if (options.url) {
|
|
219
|
+
this.connect(options.url);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** 获取当前状态 */
|
|
223
|
+
get state() {
|
|
224
|
+
return this._state;
|
|
225
|
+
}
|
|
226
|
+
/** 是否已连接 */
|
|
227
|
+
get isConnected() {
|
|
228
|
+
return this._state === EnhancedRosState.CONNECTED;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 建立连接(如已连接相同地址则忽略)
|
|
232
|
+
* @param url WebSocket 地址,例如 ws://localhost:9090
|
|
233
|
+
*/
|
|
234
|
+
connect(url) {
|
|
235
|
+
// 避免重复连接相同地址
|
|
236
|
+
if (this.currentUrl === url &&
|
|
237
|
+
(this._state === EnhancedRosState.CONNECTING || this._state === EnhancedRosState.CONNECTED)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// 进入新一轮连接生命周期
|
|
241
|
+
this.connectGeneration += 1;
|
|
242
|
+
this.cleanupForConnect();
|
|
243
|
+
this.currentUrl = url;
|
|
244
|
+
this.manualClose = false;
|
|
245
|
+
this.setState(EnhancedRosState.IDLE);
|
|
246
|
+
this.setState(EnhancedRosState.CONNECTING);
|
|
247
|
+
this.openSocket(url);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 手动关闭连接(不会触发自动重连)
|
|
251
|
+
*/
|
|
252
|
+
close() {
|
|
253
|
+
this.connectGeneration += 1;
|
|
254
|
+
this.manualClose = true;
|
|
255
|
+
this.clearReconnectTimer();
|
|
256
|
+
this.stopHeartbeat();
|
|
257
|
+
this.messageQueue = [];
|
|
258
|
+
this.setState(EnhancedRosState.CLOSED);
|
|
259
|
+
this.closeSocket();
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 发送消息(离线时自动入队)
|
|
263
|
+
* @param message 任意 JSON 兼容对象
|
|
264
|
+
*/
|
|
265
|
+
callOnConnection(message) {
|
|
266
|
+
if (this._state !== EnhancedRosState.CONNECTED || !this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
267
|
+
this.messageQueue.push(message);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.send(message);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 获取下一个自增 ID(字符串形式)
|
|
274
|
+
*/
|
|
275
|
+
getNextId() {
|
|
276
|
+
return (++this.idCounter).toString();
|
|
277
|
+
}
|
|
278
|
+
/* ===================== 私有方法 ===================== */
|
|
279
|
+
/** 状态变更并对外广播 */
|
|
280
|
+
setState(next) {
|
|
281
|
+
if (this._state === next)
|
|
282
|
+
return;
|
|
283
|
+
this._state = next;
|
|
284
|
+
this.emit('state', next);
|
|
285
|
+
}
|
|
286
|
+
/** 连接前清理资源 */
|
|
287
|
+
cleanupForConnect() {
|
|
288
|
+
this.clearReconnectTimer();
|
|
289
|
+
this.stopHeartbeat();
|
|
290
|
+
this.closeSocket();
|
|
291
|
+
this.messageQueue = [];
|
|
292
|
+
this.reconnectDelayMs = this.reconnectMinDelayMs;
|
|
293
|
+
this.lastServerMessageAtMs = null;
|
|
294
|
+
}
|
|
295
|
+
/** 清除重连定时器 */
|
|
296
|
+
clearReconnectTimer() {
|
|
297
|
+
if (!this.reconnectTimer)
|
|
298
|
+
return;
|
|
299
|
+
clearTimeout(this.reconnectTimer);
|
|
300
|
+
this.reconnectTimer = null;
|
|
301
|
+
}
|
|
302
|
+
/** 创建并绑定 WebSocket 事件 */
|
|
303
|
+
openSocket(url) {
|
|
304
|
+
var _a, _b;
|
|
305
|
+
try {
|
|
306
|
+
const WS = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.WebSocket) !== null && _b !== void 0 ? _b : WebSocket;
|
|
307
|
+
this.socket = new WS(url);
|
|
308
|
+
const generation = this.connectGeneration; // 捕获当前代际
|
|
309
|
+
this.socket.onopen = () => {
|
|
310
|
+
// 连接成功,重置退避
|
|
311
|
+
this.reconnectDelayMs = this.reconnectMinDelayMs;
|
|
312
|
+
this.lastServerMessageAtMs = Date.now();
|
|
313
|
+
this.setState(EnhancedRosState.CONNECTED);
|
|
314
|
+
this.emit('connection');
|
|
315
|
+
this.startHeartbeat();
|
|
316
|
+
this.flushQueue();
|
|
317
|
+
};
|
|
318
|
+
this.socket.onclose = () => {
|
|
319
|
+
if (generation !== this.connectGeneration)
|
|
320
|
+
return;
|
|
321
|
+
console.log('socket close');
|
|
322
|
+
this.stopHeartbeat();
|
|
323
|
+
this.socket = null;
|
|
324
|
+
this.emit('close');
|
|
325
|
+
if (this.manualClose) {
|
|
326
|
+
// 用户主动关闭,不再重连
|
|
327
|
+
this.setState(EnhancedRosState.CLOSED);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// 异常断开,进入重连逻辑
|
|
331
|
+
this.setState(EnhancedRosState.RECONNECTING);
|
|
332
|
+
this.scheduleReconnect();
|
|
333
|
+
};
|
|
334
|
+
this.socket.onerror = (error) => {
|
|
335
|
+
this.emit('error', error);
|
|
336
|
+
if (!this.manualClose && this._state === EnhancedRosState.CONNECTING) {
|
|
337
|
+
// 连接阶段出错,准备重连
|
|
338
|
+
this.setState(EnhancedRosState.RECONNECTING);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
this.socket.onmessage = (event) => {
|
|
342
|
+
this.lastServerMessageAtMs = Date.now();
|
|
343
|
+
this.handleMessage(event.data);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
this.emit('error', error);
|
|
348
|
+
this.setState(EnhancedRosState.ERROR);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** 安全关闭 WebSocket */
|
|
352
|
+
closeSocket() {
|
|
353
|
+
if (!this.socket)
|
|
354
|
+
return;
|
|
355
|
+
try {
|
|
356
|
+
this.socket.close();
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
this.socket = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** 调度下一次重连(退避策略,达到最大后固定) */
|
|
363
|
+
scheduleReconnect() {
|
|
364
|
+
if (!this.currentUrl) {
|
|
365
|
+
this.setState(EnhancedRosState.ERROR);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const generation = this.connectGeneration;
|
|
369
|
+
const delayMs = this.reconnectDelayMs;
|
|
370
|
+
this.clearReconnectTimer();
|
|
371
|
+
this.reconnectTimer = setTimeout(() => {
|
|
372
|
+
if (this.manualClose)
|
|
373
|
+
return;
|
|
374
|
+
if (generation !== this.connectGeneration)
|
|
375
|
+
return; // 连接已过期
|
|
376
|
+
if (this._state !== EnhancedRosState.RECONNECTING)
|
|
377
|
+
return;
|
|
378
|
+
if (!this.currentUrl) {
|
|
379
|
+
this.setState(EnhancedRosState.ERROR);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// 进入新一轮连接生命周期
|
|
383
|
+
// this.connectGeneration += 1;
|
|
384
|
+
// 不能 this.cleanupForConnect();
|
|
385
|
+
// 关闭定时器、清除socket在close时候已经处理; 连接上后自然会重置 this.reconnectDelayMs = this.reconnectMinDelayMs; this.lastServerMessageAtMs = null;
|
|
386
|
+
// 现在讨论是否要不要connectGeneration++ ,我认为是有必要的,每一次重连都是一个新的socket
|
|
387
|
+
// 不能 清空messageQueue,messageQueue还等着重连成功自动重发
|
|
388
|
+
this.connectGeneration += 1;
|
|
389
|
+
this.setState(EnhancedRosState.CONNECTING);
|
|
390
|
+
this.openSocket(this.currentUrl);
|
|
391
|
+
// 退避:下次等待时间翻倍,直到上限后固定
|
|
392
|
+
if (this.reconnectDelayMs < this.reconnectMaxDelayMs) {
|
|
393
|
+
this.reconnectDelayMs = Math.min(this.reconnectMaxDelayMs, this.reconnectDelayMs * 2);
|
|
394
|
+
}
|
|
395
|
+
// 已达上限,保持最大延迟不变,持续重试
|
|
396
|
+
}, delayMs);
|
|
397
|
+
}
|
|
398
|
+
/** 启动心跳定时器 */
|
|
399
|
+
startHeartbeat() {
|
|
400
|
+
this.stopHeartbeat();
|
|
401
|
+
if (this.heartbeatIntervalMs <= 0)
|
|
402
|
+
return;
|
|
403
|
+
this.heartbeatTimer = setInterval(() => {
|
|
404
|
+
if (this._state !== EnhancedRosState.CONNECTED)
|
|
405
|
+
return;
|
|
406
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
407
|
+
return;
|
|
408
|
+
const now = Date.now();
|
|
409
|
+
const last = this.lastServerMessageAtMs;
|
|
410
|
+
// 超过两倍心跳间隔未收到消息,认为连接失效
|
|
411
|
+
if (last && now - last > this.heartbeatIntervalMs * 2) {
|
|
412
|
+
this.setState(EnhancedRosState.RECONNECTING);
|
|
413
|
+
try {
|
|
414
|
+
this.closeSocket();
|
|
415
|
+
}
|
|
416
|
+
catch (_a) { }
|
|
417
|
+
// if (!this.reconnectTimer) {
|
|
418
|
+
// this.scheduleReconnect();
|
|
419
|
+
// }
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// 发送 ping 保活
|
|
423
|
+
if (this.heartbeatFn) {
|
|
424
|
+
this.heartbeatFn();
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
/// 默认心跳:调用 /rosapi/get_time 服务
|
|
428
|
+
this.cast({ op: "call_service", id: this.getNextId(), service: "/rosapi/get_time", type: "rosapi/GetTime", args: {} });
|
|
429
|
+
}
|
|
430
|
+
}, this.heartbeatIntervalMs);
|
|
431
|
+
}
|
|
432
|
+
/** 停止心跳定时器 */
|
|
433
|
+
stopHeartbeat() {
|
|
434
|
+
if (!this.heartbeatTimer)
|
|
435
|
+
return;
|
|
436
|
+
clearInterval(this.heartbeatTimer);
|
|
437
|
+
this.heartbeatTimer = null;
|
|
438
|
+
}
|
|
439
|
+
/** 将离线队列全部发出 */
|
|
440
|
+
flushQueue() {
|
|
441
|
+
if (this._state !== EnhancedRosState.CONNECTED || !this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (this.messageQueue.length === 0)
|
|
445
|
+
return;
|
|
446
|
+
const pending = this.messageQueue;
|
|
447
|
+
this.messageQueue = [];
|
|
448
|
+
for (const msg of pending) {
|
|
449
|
+
this.send(msg);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** 真正发送 JSON 字符串 */
|
|
453
|
+
send(message) {
|
|
454
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
455
|
+
// 兜底:万一状态不一致,重新入队
|
|
456
|
+
this.messageQueue.push(message);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const messageStr = JSON.stringify(message);
|
|
460
|
+
this.socket.send(messageStr);
|
|
461
|
+
}
|
|
462
|
+
cast(message) {
|
|
463
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const messageStr = JSON.stringify(message);
|
|
467
|
+
this.socket.send(messageStr);
|
|
468
|
+
}
|
|
469
|
+
/** 解析并分发服务端消息 */
|
|
470
|
+
handleMessage(data) {
|
|
471
|
+
try {
|
|
472
|
+
const message = JSON.parse(data);
|
|
473
|
+
if (message.op === 'publish') {
|
|
474
|
+
// 普通话题消息
|
|
475
|
+
this.emit(message.topic, message.msg);
|
|
476
|
+
}
|
|
477
|
+
else if (message.op === 'service_response') {
|
|
478
|
+
// 服务响应,用 id 匹配请求
|
|
479
|
+
this.emit(message.id, message);
|
|
480
|
+
}
|
|
481
|
+
else if (message.op === 'status') {
|
|
482
|
+
// 状态消息,可选带 id
|
|
483
|
+
if (message.id) {
|
|
484
|
+
this.emit('status:' + message.id, message);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
this.emit('status', message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (message.op === 'service_request') {
|
|
491
|
+
// 服务请求(作为服务端角色时)
|
|
492
|
+
this.emit('service_request:' + message.service, message);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
console.error('Error parsing message:', error);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
167
501
|
class Topic extends EventEmitter {
|
|
168
502
|
constructor(options) {
|
|
169
503
|
super();
|
|
170
504
|
this.isSubscribed = false;
|
|
171
505
|
this.isAdvertised = false;
|
|
506
|
+
/** 内部状态处理:连接关闭时重置标志位 */
|
|
507
|
+
this._handleClose = () => {
|
|
508
|
+
this.isSubscribed = false;
|
|
509
|
+
this.isAdvertised = false;
|
|
510
|
+
};
|
|
172
511
|
this.ros = options.ros;
|
|
173
512
|
this.name = options.name;
|
|
174
513
|
this.messageType = options.messageType;
|
|
@@ -177,72 +516,93 @@ class Topic extends EventEmitter {
|
|
|
177
516
|
this.queue_size = options.queue_size;
|
|
178
517
|
this.latch = options.latch;
|
|
179
518
|
this.queue_length = options.queue_length;
|
|
519
|
+
// 预定义重连逻辑
|
|
520
|
+
this._reconnectHandler = () => {
|
|
521
|
+
if (this.isSubscribed) {
|
|
522
|
+
this._sendSubscribe();
|
|
523
|
+
}
|
|
524
|
+
if (this.isAdvertised) {
|
|
525
|
+
this._sendAdvertise();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
180
528
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
529
|
+
/** 发送底层的订阅协议包 */
|
|
530
|
+
_sendSubscribe() {
|
|
531
|
+
this.isSubscribed = true;
|
|
185
532
|
const subscribeMessage = Object.assign(Object.assign(Object.assign({ op: 'subscribe', topic: this.name, type: this.messageType }, (this.compression && { compression: this.compression })), (this.throttle_rate && { throttle_rate: this.throttle_rate })), (this.queue_length && { queue_length: this.queue_length }));
|
|
186
533
|
this.ros.callOnConnection(subscribeMessage);
|
|
187
|
-
|
|
188
|
-
|
|
534
|
+
}
|
|
535
|
+
/** 发送底层的公告协议包 */
|
|
536
|
+
_sendAdvertise() {
|
|
537
|
+
this.isAdvertised = true;
|
|
538
|
+
const advertiseMessage = Object.assign(Object.assign({ op: 'advertise', topic: this.name, type: this.messageType }, (this.latch && { latch: this.latch })), (this.queue_size && { queue_size: this.queue_size }));
|
|
539
|
+
this.ros.callOnConnection(advertiseMessage);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* 订阅话题
|
|
543
|
+
* @param callback 接收消息的回调函数
|
|
544
|
+
*/
|
|
545
|
+
subscribe(callback) {
|
|
546
|
+
if (this.isSubscribed)
|
|
547
|
+
return;
|
|
548
|
+
// 1. 先尝试移除已有的监听,防止重复挂载
|
|
549
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
550
|
+
this.ros.off('close', this._handleClose);
|
|
551
|
+
// 2. 挂载监听
|
|
552
|
+
this.ros.on('connection', this._reconnectHandler);
|
|
553
|
+
this.ros.on('close', this._handleClose);
|
|
554
|
+
// 3. 执行物理订阅
|
|
555
|
+
this._sendSubscribe();
|
|
556
|
+
// 4. 监听来自 ROS 的消息分发
|
|
189
557
|
this.ros.on(this.name, (message) => {
|
|
190
558
|
this.emit('message', message);
|
|
191
|
-
if (callback)
|
|
559
|
+
if (callback)
|
|
192
560
|
callback(message);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
// 监听连接关闭事件,重新订阅
|
|
196
|
-
this.ros.on('close', () => {
|
|
197
|
-
this.isSubscribed = false;
|
|
198
|
-
});
|
|
199
|
-
this.ros.on('connection', () => {
|
|
200
|
-
if (!this.isSubscribed) {
|
|
201
|
-
this.subscribe(callback);
|
|
202
|
-
}
|
|
203
561
|
});
|
|
204
562
|
}
|
|
563
|
+
/** 取消订阅 */
|
|
205
564
|
unsubscribe() {
|
|
206
|
-
if (!this.isSubscribed)
|
|
565
|
+
if (!this.isSubscribed)
|
|
207
566
|
return;
|
|
208
|
-
}
|
|
209
567
|
const unsubscribeMessage = {
|
|
210
568
|
op: 'unsubscribe',
|
|
211
|
-
topic: this.name
|
|
569
|
+
topic: this.name,
|
|
212
570
|
};
|
|
213
571
|
this.ros.callOnConnection(unsubscribeMessage);
|
|
214
572
|
this.isSubscribed = false;
|
|
215
|
-
//
|
|
573
|
+
// 彻底清理:移除重连监听和消息监听
|
|
216
574
|
this.ros.off(this.name);
|
|
575
|
+
if (!this.isAdvertised) {
|
|
576
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
577
|
+
this.ros.off('close', this._handleClose);
|
|
578
|
+
}
|
|
217
579
|
}
|
|
580
|
+
/** 公告话题(作为发布者) */
|
|
218
581
|
advertise() {
|
|
219
|
-
if (this.isAdvertised)
|
|
582
|
+
if (this.isAdvertised)
|
|
220
583
|
return;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.ros.
|
|
224
|
-
this.
|
|
225
|
-
// 监听连接关闭事件
|
|
226
|
-
this.ros.on('close', () => {
|
|
227
|
-
this.isAdvertised = false;
|
|
228
|
-
});
|
|
229
|
-
this.ros.on('connection', () => {
|
|
230
|
-
if (!this.isAdvertised) {
|
|
231
|
-
this.advertise();
|
|
232
|
-
}
|
|
233
|
-
});
|
|
584
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
585
|
+
this.ros.on('connection', this._reconnectHandler);
|
|
586
|
+
this.ros.on('close', this._handleClose);
|
|
587
|
+
this._sendAdvertise();
|
|
234
588
|
}
|
|
589
|
+
/** 取消公告 */
|
|
235
590
|
unadvertise() {
|
|
236
|
-
if (!this.isAdvertised)
|
|
591
|
+
if (!this.isAdvertised)
|
|
237
592
|
return;
|
|
238
|
-
}
|
|
239
593
|
const unadvertiseMessage = {
|
|
240
594
|
op: 'unadvertise',
|
|
241
|
-
topic: this.name
|
|
595
|
+
topic: this.name,
|
|
242
596
|
};
|
|
243
597
|
this.ros.callOnConnection(unadvertiseMessage);
|
|
244
598
|
this.isAdvertised = false;
|
|
599
|
+
// 如果当前也没有订阅,则可以安全移除重连处理器
|
|
600
|
+
if (!this.isSubscribed) {
|
|
601
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
602
|
+
this.ros.off('close', this._handleClose);
|
|
603
|
+
}
|
|
245
604
|
}
|
|
605
|
+
/** 发布消息 */
|
|
246
606
|
publish(message) {
|
|
247
607
|
if (!this.isAdvertised) {
|
|
248
608
|
this.advertise();
|
|
@@ -250,7 +610,7 @@ class Topic extends EventEmitter {
|
|
|
250
610
|
const publishMessage = {
|
|
251
611
|
op: 'publish',
|
|
252
612
|
topic: this.name,
|
|
253
|
-
msg: message
|
|
613
|
+
msg: message,
|
|
254
614
|
};
|
|
255
615
|
this.ros.callOnConnection(publishMessage);
|
|
256
616
|
}
|
|
@@ -275,9 +635,21 @@ class Service extends EventEmitter {
|
|
|
275
635
|
constructor(options) {
|
|
276
636
|
super();
|
|
277
637
|
this.isAdvertised = false;
|
|
638
|
+
/** 存储服务请求处理函数,便于卸载 */
|
|
639
|
+
this._currentServiceCallback = null;
|
|
640
|
+
/** 内部状态处理:连接关闭时重置标志位 */
|
|
641
|
+
this._handleClose = () => {
|
|
642
|
+
this.isAdvertised = false;
|
|
643
|
+
};
|
|
278
644
|
this.ros = options.ros;
|
|
279
645
|
this.name = options.name;
|
|
280
646
|
this.serviceType = options.serviceType;
|
|
647
|
+
// 预定义重连恢复逻辑
|
|
648
|
+
this._reconnectHandler = () => {
|
|
649
|
+
if (this.isAdvertised && this._currentServiceCallback) {
|
|
650
|
+
this._sendAdvertise();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
281
653
|
}
|
|
282
654
|
callService(request, callback, failedCallback) {
|
|
283
655
|
return new Promise((resolve, reject) => {
|
|
@@ -318,18 +690,32 @@ class Service extends EventEmitter {
|
|
|
318
690
|
this.ros.callOnConnection(serviceMessage);
|
|
319
691
|
});
|
|
320
692
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
693
|
+
/** 发送底层的服务公告协议 */
|
|
694
|
+
_sendAdvertise() {
|
|
695
|
+
this.isAdvertised = true;
|
|
325
696
|
const advertiseMessage = {
|
|
326
697
|
op: 'advertise_service',
|
|
327
|
-
|
|
328
|
-
|
|
698
|
+
type: this.serviceType,
|
|
699
|
+
service: this.name
|
|
329
700
|
};
|
|
330
701
|
this.ros.callOnConnection(advertiseMessage);
|
|
331
|
-
|
|
332
|
-
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* 公告服务(服务端模式)
|
|
705
|
+
* @param callback 处理请求并返回结果的回调
|
|
706
|
+
*/
|
|
707
|
+
advertise(callback) {
|
|
708
|
+
if (this.isAdvertised)
|
|
709
|
+
return;
|
|
710
|
+
this._currentServiceCallback = callback;
|
|
711
|
+
// 1. 防御性卸载旧监听
|
|
712
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
713
|
+
this.ros.off('close', this._handleClose);
|
|
714
|
+
this.ros.off('service_request:' + this.name);
|
|
715
|
+
// 2. 挂载生命周期监听
|
|
716
|
+
this.ros.on('connection', this._reconnectHandler);
|
|
717
|
+
this.ros.on('close', this._handleClose);
|
|
718
|
+
// 3. 监听服务请求
|
|
333
719
|
this.ros.on('service_request:' + this.name, (message) => {
|
|
334
720
|
const request = new ServiceRequest(message.args);
|
|
335
721
|
const response = new ServiceResponse();
|
|
@@ -355,28 +741,26 @@ class Service extends EventEmitter {
|
|
|
355
741
|
this.ros.callOnConnection(errorMessage);
|
|
356
742
|
}
|
|
357
743
|
});
|
|
358
|
-
//
|
|
359
|
-
this.
|
|
360
|
-
this.isAdvertised = false;
|
|
361
|
-
});
|
|
362
|
-
this.ros.on('connection', () => {
|
|
363
|
-
if (!this.isAdvertised) {
|
|
364
|
-
this.advertise(callback);
|
|
365
|
-
}
|
|
366
|
-
});
|
|
744
|
+
// 4. 执行物理公告
|
|
745
|
+
this._sendAdvertise();
|
|
367
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* 取消服务公告
|
|
749
|
+
*/
|
|
368
750
|
unadvertise() {
|
|
369
|
-
if (!this.isAdvertised)
|
|
751
|
+
if (!this.isAdvertised)
|
|
370
752
|
return;
|
|
371
|
-
}
|
|
372
753
|
const unadvertiseMessage = {
|
|
373
754
|
op: 'unadvertise_service',
|
|
374
755
|
service: this.name
|
|
375
756
|
};
|
|
376
757
|
this.ros.callOnConnection(unadvertiseMessage);
|
|
377
758
|
this.isAdvertised = false;
|
|
378
|
-
|
|
759
|
+
this._currentServiceCallback = null;
|
|
760
|
+
// 彻底清理资源:移除所有相关监听
|
|
379
761
|
this.ros.off('service_request:' + this.name);
|
|
762
|
+
this.ros.off('connection', this._reconnectHandler);
|
|
763
|
+
this.ros.off('close', this._handleClose);
|
|
380
764
|
}
|
|
381
765
|
}
|
|
382
766
|
|
|
@@ -446,5 +830,209 @@ class Param {
|
|
|
446
830
|
}
|
|
447
831
|
}
|
|
448
832
|
|
|
449
|
-
|
|
833
|
+
class TopicManager {
|
|
834
|
+
constructor(ros) {
|
|
835
|
+
this.topics = new Map();
|
|
836
|
+
this.ros = ros;
|
|
837
|
+
}
|
|
838
|
+
subscribe(name, messageType, callback) {
|
|
839
|
+
if (!this.ros) {
|
|
840
|
+
throw new Error('ros instance is not initialized');
|
|
841
|
+
}
|
|
842
|
+
if (!this.ros.isConnected) {
|
|
843
|
+
console.warn(`ROS not connected, cannot subscribe to ${name}, ${name} in messageQueue when ros reconnected`);
|
|
844
|
+
}
|
|
845
|
+
// 已存在,添加回调即可
|
|
846
|
+
if (this.topics.has(name)) {
|
|
847
|
+
const managed = this.topics.get(name);
|
|
848
|
+
managed.callbacks.add(callback);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
// 创建新 topic
|
|
852
|
+
const topic = new Topic({ ros: this.ros, name, messageType });
|
|
853
|
+
const callbacks = new Set();
|
|
854
|
+
callbacks.add(callback);
|
|
855
|
+
topic.subscribe((msg) => {
|
|
856
|
+
callbacks.forEach((cb) => cb(msg));
|
|
857
|
+
});
|
|
858
|
+
this.topics.set(name, { topic, callbacks, messageType });
|
|
859
|
+
}
|
|
860
|
+
unsubscribe(name, callback) {
|
|
861
|
+
const managed = this.topics.get(name);
|
|
862
|
+
if (!managed)
|
|
863
|
+
return;
|
|
864
|
+
if (callback) {
|
|
865
|
+
managed.callbacks.delete(callback);
|
|
866
|
+
// 如果没有回调了,取消订阅
|
|
867
|
+
if (managed.callbacks.size === 0) {
|
|
868
|
+
managed.topic.unsubscribe();
|
|
869
|
+
this.topics.delete(name);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
// 取消所有订阅
|
|
874
|
+
managed.topic.unsubscribe();
|
|
875
|
+
this.topics.delete(name);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
clearAll() {
|
|
879
|
+
this.topics.forEach((managed) => {
|
|
880
|
+
managed.topic.unsubscribe();
|
|
881
|
+
});
|
|
882
|
+
this.topics.clear();
|
|
883
|
+
}
|
|
884
|
+
resubscribeAll(ros) {
|
|
885
|
+
this.topics.forEach((managed, name) => {
|
|
886
|
+
const topic = new Topic({ ros, name, messageType: managed.messageType });
|
|
887
|
+
managed.topic = topic;
|
|
888
|
+
topic.subscribe((msg) => {
|
|
889
|
+
managed.callbacks.forEach((cb) => cb(msg));
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
publish(name, messageType, data) {
|
|
894
|
+
if (!this.ros) {
|
|
895
|
+
throw new Error('ros instance is not initialized');
|
|
896
|
+
}
|
|
897
|
+
if (!this.ros.isConnected) {
|
|
898
|
+
console.warn(`ROS not connected, cannot publish to ${name}, ${name} in messageQueue when ros reconnected`);
|
|
899
|
+
}
|
|
900
|
+
const chatter = new Topic({
|
|
901
|
+
ros: this.ros,
|
|
902
|
+
name,
|
|
903
|
+
messageType
|
|
904
|
+
});
|
|
905
|
+
chatter.publish({ data: data });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
class ServiceManager {
|
|
909
|
+
constructor(ros, timeout = 10000) {
|
|
910
|
+
this.defaultTimeout = 10000; // 默认超时 10s
|
|
911
|
+
this.ros = ros;
|
|
912
|
+
this.defaultTimeout = timeout;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* 调用服务(每次直接创建 Service 实例,带统一超时)
|
|
916
|
+
*/
|
|
917
|
+
call(name, serviceType, request, timeout = this.defaultTimeout) {
|
|
918
|
+
return new Promise((resolve, reject) => {
|
|
919
|
+
if (!this.ros) {
|
|
920
|
+
return reject(new Error('ros instance is not initialized'));
|
|
921
|
+
}
|
|
922
|
+
if (!this.ros.isConnected) {
|
|
923
|
+
return reject(new Error(`ROS not connected, cannot call service ${name}`));
|
|
924
|
+
}
|
|
925
|
+
let timer = null;
|
|
926
|
+
const cleanup = () => {
|
|
927
|
+
if (timer) {
|
|
928
|
+
clearTimeout(timer);
|
|
929
|
+
timer = null;
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
timer = setTimeout(() => {
|
|
933
|
+
cleanup();
|
|
934
|
+
reject(new Error(`Service call ${name} timeout after ${timeout}ms`));
|
|
935
|
+
}, timeout);
|
|
936
|
+
try {
|
|
937
|
+
const service = new Service({
|
|
938
|
+
ros: this.ros,
|
|
939
|
+
name,
|
|
940
|
+
serviceType,
|
|
941
|
+
});
|
|
942
|
+
const serviceRequest = new ServiceRequest(request);
|
|
943
|
+
service.callService(serviceRequest, (result) => {
|
|
944
|
+
cleanup();
|
|
945
|
+
resolve(result);
|
|
946
|
+
}, (error) => {
|
|
947
|
+
cleanup();
|
|
948
|
+
reject(error);
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
cleanup();
|
|
953
|
+
reject(error);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
class ParamManager {
|
|
959
|
+
constructor(ros, timeout = 10000) {
|
|
960
|
+
this.defaultTimeout = 10000; // 默认超时 10s
|
|
961
|
+
this.ros = ros;
|
|
962
|
+
this.defaultTimeout = timeout;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* 获取参数值
|
|
966
|
+
*/
|
|
967
|
+
get(name, timeout = this.defaultTimeout) {
|
|
968
|
+
return new Promise((resolve, reject) => {
|
|
969
|
+
const ros = this.ros;
|
|
970
|
+
if (!this.ros) {
|
|
971
|
+
return reject(new Error('ros instance is not initialized'));
|
|
972
|
+
}
|
|
973
|
+
if (!this.ros.isConnected) {
|
|
974
|
+
return reject(new Error(`ROS not connected, cannot get param ${name}`));
|
|
975
|
+
}
|
|
976
|
+
const param = new Param({ ros, name });
|
|
977
|
+
const timer = setTimeout(() => {
|
|
978
|
+
reject(new Error(`Get param ${name} timeout`));
|
|
979
|
+
}, timeout);
|
|
980
|
+
param
|
|
981
|
+
.get((value) => {
|
|
982
|
+
clearTimeout(timer);
|
|
983
|
+
resolve(value);
|
|
984
|
+
})
|
|
985
|
+
.catch((error) => {
|
|
986
|
+
clearTimeout(timer);
|
|
987
|
+
reject(error);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* 设置参数值
|
|
993
|
+
*/
|
|
994
|
+
set(name, value) {
|
|
995
|
+
return new Promise((resolve, reject) => {
|
|
996
|
+
const ros = this.ros;
|
|
997
|
+
if (!this.ros) {
|
|
998
|
+
return reject(new Error('ros instance is not initialized'));
|
|
999
|
+
}
|
|
1000
|
+
if (!this.ros.isConnected) {
|
|
1001
|
+
return reject(new Error(`ROS not connected, cannot set param ${name}`));
|
|
1002
|
+
}
|
|
1003
|
+
const param = new Param({ ros, name });
|
|
1004
|
+
param
|
|
1005
|
+
.set(value, () => {
|
|
1006
|
+
resolve();
|
|
1007
|
+
})
|
|
1008
|
+
.catch((error) => {
|
|
1009
|
+
reject(error);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* 删除参数
|
|
1015
|
+
*/
|
|
1016
|
+
delete(name) {
|
|
1017
|
+
return new Promise((resolve, reject) => {
|
|
1018
|
+
const ros = this.ros;
|
|
1019
|
+
if (!this.ros) {
|
|
1020
|
+
return reject(new Error('ros instance is not initialized'));
|
|
1021
|
+
}
|
|
1022
|
+
if (!this.ros.isConnected) {
|
|
1023
|
+
return reject(new Error(`ROS not connected, cannot delete param ${name}`));
|
|
1024
|
+
}
|
|
1025
|
+
const param = new Param({ ros, name });
|
|
1026
|
+
param
|
|
1027
|
+
.delete(() => {
|
|
1028
|
+
resolve();
|
|
1029
|
+
})
|
|
1030
|
+
.catch((error) => {
|
|
1031
|
+
reject(error);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export { EnhancedRos, EnhancedRosState, EventEmitter, Param, ParamManager, Ros, Service, ServiceManager, ServiceRequest, ServiceResponse, Topic, TopicManager };
|
|
450
1038
|
//# sourceMappingURL=index.esm.js.map
|