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