@dronedeploy/rocos-js-sdk 4.4.6 → 4.4.7
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/cjs/api/StreamRegister.js +4 -4
- package/cjs/api/streams/caller/CallerStreamAbstract.js +2 -1
- package/cjs/api/streams/command/CommandStreamAbstract.js +2 -1
- package/cjs/api/streams/control/ControlStreamAbstract.js +2 -1
- package/cjs/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
- package/cjs/api/streams/search/SearchStreamAbstract.js +2 -1
- package/cjs/api/streams/telemetry/TelemetryStream.js +1 -0
- package/cjs/api/streams/telemetry/TelemetryStreamAbstract.js +126 -25
- package/cjs/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
- package/cjs/api/streams/telemetry/TelemetryStreamNode.js +1 -0
- package/cjs/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
- package/cjs/models/maps/Map.d.ts +1 -1
- package/cjs/services/SearchService.js +2 -7
- package/cjs/services/TelemetryService.d.ts +4 -0
- package/cjs/services/TelemetryService.js +12 -2
- package/esm/api/StreamRegister.js +4 -4
- package/esm/api/streams/caller/CallerStreamAbstract.js +2 -1
- package/esm/api/streams/command/CommandStreamAbstract.js +2 -1
- package/esm/api/streams/control/ControlStreamAbstract.js +2 -1
- package/esm/api/streams/fileAccessor/FileAccessorStreamAbstract.js +2 -1
- package/esm/api/streams/search/SearchStreamAbstract.js +2 -1
- package/esm/api/streams/telemetry/TelemetryStream.js +1 -0
- package/esm/api/streams/telemetry/TelemetryStreamAbstract.js +127 -26
- package/esm/api/streams/telemetry/TelemetryStreamConnect.js +1 -0
- package/esm/api/streams/telemetry/TelemetryStreamNode.js +1 -0
- package/esm/api/streams/webRTCSignalling/WebRTCSignallingStreamAbstract.js +2 -1
- package/esm/models/maps/Map.d.ts +1 -1
- package/esm/services/SearchService.js +2 -7
- package/esm/services/TelemetryService.d.ts +4 -0
- package/esm/services/TelemetryService.js +12 -2
- package/package.json +1 -1
|
@@ -82,13 +82,13 @@ class StreamRegister {
|
|
|
82
82
|
return;
|
|
83
83
|
// Wait for init to settle before tearing down, so init can never open resources on an already
|
|
84
84
|
// stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
|
|
85
|
-
|
|
86
|
-
.catch(() => undefined)
|
|
87
|
-
.then(() => {
|
|
85
|
+
const evictIfUnheld = () => {
|
|
88
86
|
if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
|
|
89
87
|
this.evict(identifier, entry.stream);
|
|
90
88
|
}
|
|
91
|
-
}
|
|
89
|
+
};
|
|
90
|
+
void entry.ready
|
|
91
|
+
.then(() => evictIfUnheld(), () => evictIfUnheld())
|
|
92
92
|
.catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
|
|
93
93
|
}
|
|
94
94
|
evict(identifier, stream) {
|
|
@@ -29,7 +29,8 @@ class CallerStreamAbstract {
|
|
|
29
29
|
this.token = token;
|
|
30
30
|
}
|
|
31
31
|
stopStream() {
|
|
32
|
-
|
|
32
|
+
this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
|
|
33
|
+
this.statusStream$.complete();
|
|
33
34
|
}
|
|
34
35
|
invokeRequest(params) {
|
|
35
36
|
this.logger.info('invokeRequest', params);
|
|
@@ -31,7 +31,8 @@ class CommandStreamAbstract {
|
|
|
31
31
|
this.token = token;
|
|
32
32
|
}
|
|
33
33
|
stopStream() {
|
|
34
|
-
|
|
34
|
+
this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
|
|
35
|
+
this.statusStream$.complete();
|
|
35
36
|
}
|
|
36
37
|
invokeRequest(params) {
|
|
37
38
|
if (params.timeoutMs === undefined || params.timeoutMs === null) {
|
|
@@ -28,7 +28,8 @@ class ControlStreamAbstract {
|
|
|
28
28
|
this.token = token;
|
|
29
29
|
}
|
|
30
30
|
stopStream() {
|
|
31
|
-
|
|
31
|
+
this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
|
|
32
|
+
this.statusStream$.complete();
|
|
32
33
|
}
|
|
33
34
|
takeActionWithAck(action, params, ackTimeoutInSeconds = 15) {
|
|
34
35
|
return new rxjs_1.Observable((emitter) => {
|
|
@@ -27,7 +27,8 @@ class FileAccessorStreamAbstract {
|
|
|
27
27
|
this.token = token;
|
|
28
28
|
}
|
|
29
29
|
stopStream() {
|
|
30
|
-
|
|
30
|
+
this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
|
|
31
|
+
this.statusStream$.complete();
|
|
31
32
|
}
|
|
32
33
|
async serverReqOp(params) {
|
|
33
34
|
return this.serverReqOpInternal(params);
|
|
@@ -28,7 +28,8 @@ class SearchStreamAbstract {
|
|
|
28
28
|
this.token = token;
|
|
29
29
|
}
|
|
30
30
|
stopStream() {
|
|
31
|
-
|
|
31
|
+
this.statusStream$.next(models_1.SubscriberStatusEnum.STOPPED);
|
|
32
|
+
this.statusStream$.complete();
|
|
32
33
|
}
|
|
33
34
|
searchStream(params) {
|
|
34
35
|
const request = this.convertToQueryMessage(params.query);
|
|
@@ -24,6 +24,7 @@ class TelemetryStream extends TelemetryStreamAbstract_1.TelemetryStreamAbstract
|
|
|
24
24
|
this.client = new teletubby_pb_client_1.TelemetryGatewayClient(transport);
|
|
25
25
|
}
|
|
26
26
|
stopInternal() {
|
|
27
|
+
this.subscriberId = undefined;
|
|
27
28
|
if (this.abortController) {
|
|
28
29
|
this.logger.info('Starting stop stream process');
|
|
29
30
|
this.abortController.abort();
|
|
@@ -13,6 +13,33 @@ const arrayRemove_1 = require("../../../helpers/arrayRemove");
|
|
|
13
13
|
const arrayUnique_1 = require("../../../helpers/arrayUnique");
|
|
14
14
|
const operators_1 = require("rxjs/operators");
|
|
15
15
|
const getSubscriptionsDifference_1 = require("../../../helpers/getSubscriptionsDifference");
|
|
16
|
+
const telemetryActionQueueStates = new WeakMap();
|
|
17
|
+
const resolveTelemetryActions = (actions) => {
|
|
18
|
+
actions.forEach((action) => action.resolve());
|
|
19
|
+
};
|
|
20
|
+
const resolveUnsentTelemetryActions = (queueState) => {
|
|
21
|
+
resolveTelemetryActions(queueState.pendingActions.splice(0));
|
|
22
|
+
};
|
|
23
|
+
const takeNextTelemetryBatch = (queueState) => {
|
|
24
|
+
const firstAction = queueState.pendingActions.shift();
|
|
25
|
+
if (!firstAction)
|
|
26
|
+
return [];
|
|
27
|
+
const activeBatch = [firstAction];
|
|
28
|
+
if (firstAction.callsignsLookup.isQuery() || firstAction.terminate)
|
|
29
|
+
return activeBatch;
|
|
30
|
+
while (queueState.pendingActions.length > 0) {
|
|
31
|
+
const [nextAction] = queueState.pendingActions;
|
|
32
|
+
if (nextAction.callsignsLookup.isQuery() ||
|
|
33
|
+
nextAction.terminate ||
|
|
34
|
+
nextAction.actionType !== firstAction.actionType ||
|
|
35
|
+
nextAction.subscriberId !== firstAction.subscriberId ||
|
|
36
|
+
nextAction.registration !== firstAction.registration) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
activeBatch.push(queueState.pendingActions.shift());
|
|
40
|
+
}
|
|
41
|
+
return activeBatch;
|
|
42
|
+
};
|
|
16
43
|
class TelemetryStreamAbstract {
|
|
17
44
|
constructor(config) {
|
|
18
45
|
this.telemetryActionQueue = Promise.resolve();
|
|
@@ -38,6 +65,14 @@ class TelemetryStreamAbstract {
|
|
|
38
65
|
this.messageStream$ = new rxjs_1.Subject();
|
|
39
66
|
this.statusStream$ = new rxjs_1.BehaviorSubject(models_1.SubscriberStatusEnum.STOPPED);
|
|
40
67
|
this.heartbeat = new StreamHeartbeat_1.StreamHeartbeat(this.identifier);
|
|
68
|
+
telemetryActionQueueStates.set(this, {
|
|
69
|
+
pendingActions: [],
|
|
70
|
+
activeBatch: [],
|
|
71
|
+
registration: 0,
|
|
72
|
+
drainScheduled: false,
|
|
73
|
+
requestInFlight: false,
|
|
74
|
+
stopped: false,
|
|
75
|
+
});
|
|
41
76
|
RocosStore_1.RocosStore.getChangeSubject()
|
|
42
77
|
.pipe((0, operators_1.filter)((message) => {
|
|
43
78
|
return message.type === 'token' && message.url === this.url;
|
|
@@ -55,6 +90,12 @@ class TelemetryStreamAbstract {
|
|
|
55
90
|
this.registerReceiver();
|
|
56
91
|
}
|
|
57
92
|
stopStream() {
|
|
93
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
94
|
+
if (queueState) {
|
|
95
|
+
queueState.stopped = true;
|
|
96
|
+
resolveUnsentTelemetryActions(queueState);
|
|
97
|
+
}
|
|
98
|
+
this.subscriberId = undefined;
|
|
58
99
|
this.heartbeat?.stop();
|
|
59
100
|
this.stopInternal();
|
|
60
101
|
if (this.messagesTimer) {
|
|
@@ -87,7 +128,9 @@ class TelemetryStreamAbstract {
|
|
|
87
128
|
const toAdd = (0, arrayRemove_1.arrayRemove)(this.sources, sourcesBefore);
|
|
88
129
|
if (toAdd.length) {
|
|
89
130
|
this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
|
|
90
|
-
void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd)
|
|
131
|
+
void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
|
|
132
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
133
|
+
});
|
|
91
134
|
}
|
|
92
135
|
}
|
|
93
136
|
else {
|
|
@@ -98,7 +141,9 @@ class TelemetryStreamAbstract {
|
|
|
98
141
|
const { toAdd } = (0, getSubscriptionsDifference_1.getSubscriptionsDifference)(before, after);
|
|
99
142
|
if (toAdd.callsigns.length && toAdd.sources.length) {
|
|
100
143
|
this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
|
|
101
|
-
void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources)
|
|
144
|
+
void this.enqueueTelemetryAction('subscribe', new models_1.CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
|
|
145
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
146
|
+
});
|
|
102
147
|
}
|
|
103
148
|
}
|
|
104
149
|
}
|
|
@@ -204,14 +249,22 @@ class TelemetryStreamAbstract {
|
|
|
204
249
|
// Subscribe message
|
|
205
250
|
case grpc_1.GRPC_SOURCE_SUBSCRIBED: {
|
|
206
251
|
const { subscriberId } = rocosMessage.payload;
|
|
252
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
253
|
+
if (queueState?.stopped)
|
|
254
|
+
return;
|
|
207
255
|
if (this.subscriberId && subscriberId) {
|
|
208
256
|
this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
|
|
209
257
|
}
|
|
210
|
-
|
|
258
|
+
if (queueState) {
|
|
259
|
+
queueState.registration++;
|
|
260
|
+
resolveUnsentTelemetryActions(queueState);
|
|
261
|
+
}
|
|
211
262
|
this.subscriberId = subscriberId;
|
|
212
263
|
rocosMessage.subscriberId = this.subscriberId;
|
|
213
264
|
this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
|
|
214
|
-
void this.enqueueTelemetryAction('subscribe')
|
|
265
|
+
void this.enqueueTelemetryAction('subscribe').catch((error) => {
|
|
266
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
267
|
+
});
|
|
215
268
|
this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
|
|
216
269
|
isRobotMessage = false;
|
|
217
270
|
break;
|
|
@@ -253,21 +306,69 @@ class TelemetryStreamAbstract {
|
|
|
253
306
|
* unsubscribe landing last strands a subscribe and nothing re-adds it.
|
|
254
307
|
*/
|
|
255
308
|
enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
309
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
310
|
+
if (!queueState || queueState.stopped)
|
|
311
|
+
return Promise.resolve();
|
|
312
|
+
const completion = new Promise((resolve, reject) => {
|
|
313
|
+
queueState.pendingActions.push({
|
|
314
|
+
actionType,
|
|
315
|
+
callsignsLookup: callsignsLookup ?? this.callsignsLookup,
|
|
316
|
+
sources: [...(sources ?? this.sources)],
|
|
317
|
+
terminate: terminate ?? false,
|
|
318
|
+
subscriberId: this.subscriberId,
|
|
319
|
+
registration: queueState.registration,
|
|
320
|
+
resolve,
|
|
321
|
+
reject,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const scheduleDrain = () => {
|
|
325
|
+
if (queueState.stopped ||
|
|
326
|
+
queueState.requestInFlight ||
|
|
327
|
+
queueState.drainScheduled ||
|
|
328
|
+
queueState.pendingActions.length === 0) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
queueState.drainScheduled = true;
|
|
332
|
+
this.telemetryActionQueue = this.telemetryActionQueue.then(async () => {
|
|
333
|
+
queueState.drainScheduled = false;
|
|
334
|
+
if (queueState.stopped || queueState.requestInFlight)
|
|
335
|
+
return;
|
|
336
|
+
const activeBatch = takeNextTelemetryBatch(queueState);
|
|
337
|
+
const [firstAction] = activeBatch;
|
|
338
|
+
if (!firstAction)
|
|
339
|
+
return;
|
|
340
|
+
queueState.activeBatch = activeBatch;
|
|
341
|
+
queueState.requestInFlight = true;
|
|
342
|
+
try {
|
|
343
|
+
await this.takeTelemetryAction(firstAction.actionType, firstAction.callsignsLookup, firstAction.sources);
|
|
344
|
+
resolveTelemetryActions(activeBatch);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
activeBatch.forEach((action) => action.reject(error));
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
queueState.activeBatch = [];
|
|
351
|
+
queueState.requestInFlight = false;
|
|
352
|
+
scheduleDrain();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
scheduleDrain();
|
|
357
|
+
return completion;
|
|
262
358
|
}
|
|
263
|
-
async takeTelemetryAction(actionType, callsignsLookup, sources
|
|
359
|
+
async takeTelemetryAction(actionType, callsignsLookup, sources) {
|
|
264
360
|
this.logger.debug('takeTelemetryAction', actionType);
|
|
361
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
362
|
+
const activeBatch = queueState?.activeBatch ?? [];
|
|
363
|
+
const [firstAction] = activeBatch;
|
|
265
364
|
// A config fault, not a race: this stream can never address a request.
|
|
266
365
|
if (!this.projectId) {
|
|
267
366
|
this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
|
|
268
367
|
return Promise.resolve();
|
|
269
368
|
}
|
|
270
|
-
if (!
|
|
369
|
+
if (!firstAction?.subscriberId ||
|
|
370
|
+
firstAction.subscriberId !== this.subscriberId ||
|
|
371
|
+
firstAction.registration !== queueState?.registration) {
|
|
271
372
|
// Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
|
|
272
373
|
this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
|
|
273
374
|
return Promise.resolve();
|
|
@@ -275,20 +376,20 @@ class TelemetryStreamAbstract {
|
|
|
275
376
|
const actionSources = sources ?? this.sources;
|
|
276
377
|
const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
|
|
277
378
|
if (!actionCallsigns.isQuery()) {
|
|
278
|
-
// Compose the request message
|
|
279
379
|
const req = teletubby_pb_1.TelemetryRequest.create({
|
|
280
|
-
subscriberId:
|
|
281
|
-
requestedActions:
|
|
282
|
-
{
|
|
283
|
-
operation: actionType,
|
|
284
|
-
callsigns:
|
|
285
|
-
sources:
|
|
286
|
-
}
|
|
287
|
-
|
|
380
|
+
subscriberId: firstAction.subscriberId,
|
|
381
|
+
requestedActions: activeBatch.map((action) => {
|
|
382
|
+
const telemetryAction = teletubby_pb_1.TelemetryAction.create({
|
|
383
|
+
operation: action.actionType,
|
|
384
|
+
callsigns: action.callsignsLookup.getCallsigns(),
|
|
385
|
+
sources: action.sources,
|
|
386
|
+
});
|
|
387
|
+
if (action.actionType === 'unsubscribe' && action.terminate) {
|
|
388
|
+
this.addTerminateToAction(telemetryAction);
|
|
389
|
+
}
|
|
390
|
+
return telemetryAction;
|
|
391
|
+
}),
|
|
288
392
|
});
|
|
289
|
-
if (actionType === 'unsubscribe' && terminate) {
|
|
290
|
-
this.addTerminateToAction(req.requestedActions[0]);
|
|
291
|
-
}
|
|
292
393
|
this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
|
|
293
394
|
// Send the message to back-end.
|
|
294
395
|
return this.requestTelemetry(req);
|
|
@@ -313,7 +414,7 @@ class TelemetryStreamAbstract {
|
|
|
313
414
|
});
|
|
314
415
|
// Compose the request message
|
|
315
416
|
const req = teletubby_pb_1.TelemetryQueryRequest.create({
|
|
316
|
-
subscriberId:
|
|
417
|
+
subscriberId: firstAction.subscriberId,
|
|
317
418
|
sources: actionSources,
|
|
318
419
|
operation: actionType,
|
|
319
420
|
callsignQuery: {
|
|
@@ -40,6 +40,7 @@ class TelemetryStreamConnect extends TelemetryStreamAbstract_1.TelemetryStreamAb
|
|
|
40
40
|
this.client = (0, connect_1.createClient)(teletubby_pb_1.TelemetryGateway, transport);
|
|
41
41
|
}
|
|
42
42
|
stopInternal() {
|
|
43
|
+
this.subscriberId = undefined;
|
|
43
44
|
if (this.abortController) {
|
|
44
45
|
this.logger.info('Starting stop stream process');
|
|
45
46
|
this.abortController.abort();
|
|
@@ -48,6 +48,7 @@ class TelemetryStreamNode extends TelemetryStreamAbstract_1.TelemetryStreamAbstr
|
|
|
48
48
|
this.credentials = config.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl();
|
|
49
49
|
}
|
|
50
50
|
stopInternal() {
|
|
51
|
+
this.subscriberId = undefined;
|
|
51
52
|
try {
|
|
52
53
|
this.logger.info('Starting stop stream process');
|
|
53
54
|
this.logger.info('Stopping stream');
|
package/cjs/models/maps/Map.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SearchService = void 0;
|
|
4
|
-
const models_1 = require("../models");
|
|
5
4
|
const rxjs_1 = require("rxjs");
|
|
6
5
|
const BaseStreamService_1 = require("./BaseStreamService");
|
|
7
6
|
const identifier_1 = require("../constants/identifier");
|
|
@@ -47,12 +46,8 @@ class SearchService extends BaseStreamService_1.BaseStreamService {
|
|
|
47
46
|
port: this.config.port,
|
|
48
47
|
insecure: this.config.insecure,
|
|
49
48
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
|
|
53
|
-
this.status$.next(msg);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
49
|
+
// The status forwarder is wired once per stream by BaseStreamService.acquireStream's factory
|
|
50
|
+
// (and released now that the stream completes statusStream$ on stop), so no per-service one here.
|
|
56
51
|
return newStream.stream;
|
|
57
52
|
}
|
|
58
53
|
getStream(config) {
|
|
@@ -28,6 +28,10 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
|
|
|
28
28
|
getStatusChanges(): Observable<IStreamStatusMessage>;
|
|
29
29
|
/**
|
|
30
30
|
* Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
|
|
31
|
+
*
|
|
32
|
+
* @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
|
|
33
|
+
* normalised robot status (reported health/readiness plus platform connectivity) rather than only
|
|
34
|
+
* heartbeat-derived online/offline liveness.
|
|
31
35
|
* @param projectId
|
|
32
36
|
* @param callsign
|
|
33
37
|
* @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
|
|
@@ -88,8 +88,14 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
|
|
|
88
88
|
return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
|
|
89
89
|
return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
|
|
90
90
|
}), (0, operators_1.finalize)(() => {
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
if (params.terminateReceiverGroupOnUnsubscribe) {
|
|
92
|
+
const removal = stream.removeSubscription(subscriptionParams, true);
|
|
93
|
+
void Promise.resolve(removal).finally(release);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
release();
|
|
97
|
+
void stream.removeSubscription(subscriptionParams);
|
|
98
|
+
}
|
|
93
99
|
}));
|
|
94
100
|
});
|
|
95
101
|
}
|
|
@@ -111,6 +117,10 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
|
|
|
111
117
|
}
|
|
112
118
|
/**
|
|
113
119
|
* Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
|
|
120
|
+
*
|
|
121
|
+
* @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
|
|
122
|
+
* normalised robot status (reported health/readiness plus platform connectivity) rather than only
|
|
123
|
+
* heartbeat-derived online/offline liveness.
|
|
114
124
|
* @param projectId
|
|
115
125
|
* @param callsign
|
|
116
126
|
* @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
|
|
@@ -79,13 +79,13 @@ export class StreamRegister {
|
|
|
79
79
|
return;
|
|
80
80
|
// Wait for init to settle before tearing down, so init can never open resources on an already
|
|
81
81
|
// stopped stream. A re-acquire in the meantime revives the entry and cancels the teardown.
|
|
82
|
-
|
|
83
|
-
.catch(() => undefined)
|
|
84
|
-
.then(() => {
|
|
82
|
+
const evictIfUnheld = () => {
|
|
85
83
|
if (this.entries.get(identifier) === entry && entry.refCount <= 0) {
|
|
86
84
|
this.evict(identifier, entry.stream);
|
|
87
85
|
}
|
|
88
|
-
}
|
|
86
|
+
};
|
|
87
|
+
void entry.ready
|
|
88
|
+
.then(() => evictIfUnheld(), () => evictIfUnheld())
|
|
89
89
|
.catch((e) => this.logger.error(`Failed to evict stream. identifier: ${identifier}`, e));
|
|
90
90
|
}
|
|
91
91
|
evict(identifier, stream) {
|
|
@@ -26,7 +26,8 @@ export class CallerStreamAbstract {
|
|
|
26
26
|
this.token = token;
|
|
27
27
|
}
|
|
28
28
|
stopStream() {
|
|
29
|
-
|
|
29
|
+
this.statusStream$.next(SubscriberStatusEnum.STOPPED);
|
|
30
|
+
this.statusStream$.complete();
|
|
30
31
|
}
|
|
31
32
|
invokeRequest(params) {
|
|
32
33
|
this.logger.info('invokeRequest', params);
|
|
@@ -28,7 +28,8 @@ export class CommandStreamAbstract {
|
|
|
28
28
|
this.token = token;
|
|
29
29
|
}
|
|
30
30
|
stopStream() {
|
|
31
|
-
|
|
31
|
+
this.statusStream$.next(SubscriberStatusEnum.STOPPED);
|
|
32
|
+
this.statusStream$.complete();
|
|
32
33
|
}
|
|
33
34
|
invokeRequest(params) {
|
|
34
35
|
if (params.timeoutMs === undefined || params.timeoutMs === null) {
|
|
@@ -25,7 +25,8 @@ export class ControlStreamAbstract {
|
|
|
25
25
|
this.token = token;
|
|
26
26
|
}
|
|
27
27
|
stopStream() {
|
|
28
|
-
|
|
28
|
+
this.statusStream$.next(SubscriberStatusEnum.STOPPED);
|
|
29
|
+
this.statusStream$.complete();
|
|
29
30
|
}
|
|
30
31
|
takeActionWithAck(action, params, ackTimeoutInSeconds = 15) {
|
|
31
32
|
return new Observable((emitter) => {
|
|
@@ -24,7 +24,8 @@ export class FileAccessorStreamAbstract {
|
|
|
24
24
|
this.token = token;
|
|
25
25
|
}
|
|
26
26
|
stopStream() {
|
|
27
|
-
|
|
27
|
+
this.statusStream$.next(SubscriberStatusEnum.STOPPED);
|
|
28
|
+
this.statusStream$.complete();
|
|
28
29
|
}
|
|
29
30
|
async serverReqOp(params) {
|
|
30
31
|
return this.serverReqOpInternal(params);
|
|
@@ -25,7 +25,8 @@ export class SearchStreamAbstract {
|
|
|
25
25
|
this.token = token;
|
|
26
26
|
}
|
|
27
27
|
stopStream() {
|
|
28
|
-
|
|
28
|
+
this.statusStream$.next(SubscriberStatusEnum.STOPPED);
|
|
29
|
+
this.statusStream$.complete();
|
|
29
30
|
}
|
|
30
31
|
searchStream(params) {
|
|
31
32
|
const request = this.convertToQueryMessage(params.query);
|
|
@@ -21,6 +21,7 @@ export class TelemetryStream extends TelemetryStreamAbstract {
|
|
|
21
21
|
this.client = new TelemetryGatewayClient(transport);
|
|
22
22
|
}
|
|
23
23
|
stopInternal() {
|
|
24
|
+
this.subscriberId = undefined;
|
|
24
25
|
if (this.abortController) {
|
|
25
26
|
this.logger.info('Starting stop stream process');
|
|
26
27
|
this.abortController.abort();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
2
2
|
import { CallsignsLookup, RocosTelemetryMessage, StreamOptions, SubscriberStatusEnum, } from '../../../models';
|
|
3
3
|
import { GRPC_SOURCE_NOOP, GRPC_SOURCE_SUBSCRIBED } from '../../../constants/grpc';
|
|
4
|
-
import { QueryOrPredicate, TelemetryAckStatus, TelemetryQueryRequest, TelemetryRequest, UnsubscribeOperation, } from '../../../grpc/teletubby_pb';
|
|
4
|
+
import { QueryOrPredicate, TelemetryAckStatus, TelemetryAction, TelemetryQueryRequest, TelemetryRequest, UnsubscribeOperation, } from '../../../grpc/teletubby_pb';
|
|
5
5
|
import { IDENTIFIER_NAME_TELEMETRY } from '../../../constants/identifier';
|
|
6
6
|
import { RocosStore } from '../../../store/RocosStore';
|
|
7
7
|
import { StreamHeartbeat } from '../../atoms/StreamHeartbeat';
|
|
@@ -10,6 +10,33 @@ import { arrayRemove } from '../../../helpers/arrayRemove';
|
|
|
10
10
|
import { arrayUnique } from '../../../helpers/arrayUnique';
|
|
11
11
|
import { filter } from 'rxjs/operators';
|
|
12
12
|
import { getSubscriptionsDifference } from '../../../helpers/getSubscriptionsDifference';
|
|
13
|
+
const telemetryActionQueueStates = new WeakMap();
|
|
14
|
+
const resolveTelemetryActions = (actions) => {
|
|
15
|
+
actions.forEach((action) => action.resolve());
|
|
16
|
+
};
|
|
17
|
+
const resolveUnsentTelemetryActions = (queueState) => {
|
|
18
|
+
resolveTelemetryActions(queueState.pendingActions.splice(0));
|
|
19
|
+
};
|
|
20
|
+
const takeNextTelemetryBatch = (queueState) => {
|
|
21
|
+
const firstAction = queueState.pendingActions.shift();
|
|
22
|
+
if (!firstAction)
|
|
23
|
+
return [];
|
|
24
|
+
const activeBatch = [firstAction];
|
|
25
|
+
if (firstAction.callsignsLookup.isQuery() || firstAction.terminate)
|
|
26
|
+
return activeBatch;
|
|
27
|
+
while (queueState.pendingActions.length > 0) {
|
|
28
|
+
const [nextAction] = queueState.pendingActions;
|
|
29
|
+
if (nextAction.callsignsLookup.isQuery() ||
|
|
30
|
+
nextAction.terminate ||
|
|
31
|
+
nextAction.actionType !== firstAction.actionType ||
|
|
32
|
+
nextAction.subscriberId !== firstAction.subscriberId ||
|
|
33
|
+
nextAction.registration !== firstAction.registration) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
activeBatch.push(queueState.pendingActions.shift());
|
|
37
|
+
}
|
|
38
|
+
return activeBatch;
|
|
39
|
+
};
|
|
13
40
|
export class TelemetryStreamAbstract {
|
|
14
41
|
constructor(config) {
|
|
15
42
|
this.telemetryActionQueue = Promise.resolve();
|
|
@@ -35,6 +62,14 @@ export class TelemetryStreamAbstract {
|
|
|
35
62
|
this.messageStream$ = new Subject();
|
|
36
63
|
this.statusStream$ = new BehaviorSubject(SubscriberStatusEnum.STOPPED);
|
|
37
64
|
this.heartbeat = new StreamHeartbeat(this.identifier);
|
|
65
|
+
telemetryActionQueueStates.set(this, {
|
|
66
|
+
pendingActions: [],
|
|
67
|
+
activeBatch: [],
|
|
68
|
+
registration: 0,
|
|
69
|
+
drainScheduled: false,
|
|
70
|
+
requestInFlight: false,
|
|
71
|
+
stopped: false,
|
|
72
|
+
});
|
|
38
73
|
RocosStore.getChangeSubject()
|
|
39
74
|
.pipe(filter((message) => {
|
|
40
75
|
return message.type === 'token' && message.url === this.url;
|
|
@@ -52,6 +87,12 @@ export class TelemetryStreamAbstract {
|
|
|
52
87
|
this.registerReceiver();
|
|
53
88
|
}
|
|
54
89
|
stopStream() {
|
|
90
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
91
|
+
if (queueState) {
|
|
92
|
+
queueState.stopped = true;
|
|
93
|
+
resolveUnsentTelemetryActions(queueState);
|
|
94
|
+
}
|
|
95
|
+
this.subscriberId = undefined;
|
|
55
96
|
this.heartbeat?.stop();
|
|
56
97
|
this.stopInternal();
|
|
57
98
|
if (this.messagesTimer) {
|
|
@@ -84,7 +125,9 @@ export class TelemetryStreamAbstract {
|
|
|
84
125
|
const toAdd = arrayRemove(this.sources, sourcesBefore);
|
|
85
126
|
if (toAdd.length) {
|
|
86
127
|
this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
|
|
87
|
-
void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd)
|
|
128
|
+
void this.enqueueTelemetryAction('subscribe', this.callsignsLookup, toAdd).catch((error) => {
|
|
129
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
130
|
+
});
|
|
88
131
|
}
|
|
89
132
|
}
|
|
90
133
|
else {
|
|
@@ -95,7 +138,9 @@ export class TelemetryStreamAbstract {
|
|
|
95
138
|
const { toAdd } = getSubscriptionsDifference(before, after);
|
|
96
139
|
if (toAdd.callsigns.length && toAdd.sources.length) {
|
|
97
140
|
this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
|
|
98
|
-
void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources)
|
|
141
|
+
void this.enqueueTelemetryAction('subscribe', new CallsignsLookup(toAdd.callsigns), toAdd.sources).catch((error) => {
|
|
142
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
143
|
+
});
|
|
99
144
|
}
|
|
100
145
|
}
|
|
101
146
|
}
|
|
@@ -201,14 +246,22 @@ export class TelemetryStreamAbstract {
|
|
|
201
246
|
// Subscribe message
|
|
202
247
|
case GRPC_SOURCE_SUBSCRIBED: {
|
|
203
248
|
const { subscriberId } = rocosMessage.payload;
|
|
249
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
250
|
+
if (queueState?.stopped)
|
|
251
|
+
return;
|
|
204
252
|
if (this.subscriberId && subscriberId) {
|
|
205
253
|
this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
|
|
206
254
|
}
|
|
207
|
-
|
|
255
|
+
if (queueState) {
|
|
256
|
+
queueState.registration++;
|
|
257
|
+
resolveUnsentTelemetryActions(queueState);
|
|
258
|
+
}
|
|
208
259
|
this.subscriberId = subscriberId;
|
|
209
260
|
rocosMessage.subscriberId = this.subscriberId;
|
|
210
261
|
this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
|
|
211
|
-
void this.enqueueTelemetryAction('subscribe')
|
|
262
|
+
void this.enqueueTelemetryAction('subscribe').catch((error) => {
|
|
263
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
264
|
+
});
|
|
212
265
|
this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
|
|
213
266
|
isRobotMessage = false;
|
|
214
267
|
break;
|
|
@@ -250,21 +303,69 @@ export class TelemetryStreamAbstract {
|
|
|
250
303
|
* unsubscribe landing last strands a subscribe and nothing re-adds it.
|
|
251
304
|
*/
|
|
252
305
|
enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
307
|
+
if (!queueState || queueState.stopped)
|
|
308
|
+
return Promise.resolve();
|
|
309
|
+
const completion = new Promise((resolve, reject) => {
|
|
310
|
+
queueState.pendingActions.push({
|
|
311
|
+
actionType,
|
|
312
|
+
callsignsLookup: callsignsLookup ?? this.callsignsLookup,
|
|
313
|
+
sources: [...(sources ?? this.sources)],
|
|
314
|
+
terminate: terminate ?? false,
|
|
315
|
+
subscriberId: this.subscriberId,
|
|
316
|
+
registration: queueState.registration,
|
|
317
|
+
resolve,
|
|
318
|
+
reject,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
const scheduleDrain = () => {
|
|
322
|
+
if (queueState.stopped ||
|
|
323
|
+
queueState.requestInFlight ||
|
|
324
|
+
queueState.drainScheduled ||
|
|
325
|
+
queueState.pendingActions.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
queueState.drainScheduled = true;
|
|
329
|
+
this.telemetryActionQueue = this.telemetryActionQueue.then(async () => {
|
|
330
|
+
queueState.drainScheduled = false;
|
|
331
|
+
if (queueState.stopped || queueState.requestInFlight)
|
|
332
|
+
return;
|
|
333
|
+
const activeBatch = takeNextTelemetryBatch(queueState);
|
|
334
|
+
const [firstAction] = activeBatch;
|
|
335
|
+
if (!firstAction)
|
|
336
|
+
return;
|
|
337
|
+
queueState.activeBatch = activeBatch;
|
|
338
|
+
queueState.requestInFlight = true;
|
|
339
|
+
try {
|
|
340
|
+
await this.takeTelemetryAction(firstAction.actionType, firstAction.callsignsLookup, firstAction.sources);
|
|
341
|
+
resolveTelemetryActions(activeBatch);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
activeBatch.forEach((action) => action.reject(error));
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
queueState.activeBatch = [];
|
|
348
|
+
queueState.requestInFlight = false;
|
|
349
|
+
scheduleDrain();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
scheduleDrain();
|
|
354
|
+
return completion;
|
|
259
355
|
}
|
|
260
|
-
async takeTelemetryAction(actionType, callsignsLookup, sources
|
|
356
|
+
async takeTelemetryAction(actionType, callsignsLookup, sources) {
|
|
261
357
|
this.logger.debug('takeTelemetryAction', actionType);
|
|
358
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
359
|
+
const activeBatch = queueState?.activeBatch ?? [];
|
|
360
|
+
const [firstAction] = activeBatch;
|
|
262
361
|
// A config fault, not a race: this stream can never address a request.
|
|
263
362
|
if (!this.projectId) {
|
|
264
363
|
this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
|
|
265
364
|
return Promise.resolve();
|
|
266
365
|
}
|
|
267
|
-
if (!
|
|
366
|
+
if (!firstAction?.subscriberId ||
|
|
367
|
+
firstAction.subscriberId !== this.subscriberId ||
|
|
368
|
+
firstAction.registration !== queueState?.registration) {
|
|
268
369
|
// Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
|
|
269
370
|
this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
|
|
270
371
|
return Promise.resolve();
|
|
@@ -272,20 +373,20 @@ export class TelemetryStreamAbstract {
|
|
|
272
373
|
const actionSources = sources ?? this.sources;
|
|
273
374
|
const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
|
|
274
375
|
if (!actionCallsigns.isQuery()) {
|
|
275
|
-
// Compose the request message
|
|
276
376
|
const req = TelemetryRequest.create({
|
|
277
|
-
subscriberId:
|
|
278
|
-
requestedActions:
|
|
279
|
-
{
|
|
280
|
-
operation: actionType,
|
|
281
|
-
callsigns:
|
|
282
|
-
sources:
|
|
283
|
-
}
|
|
284
|
-
|
|
377
|
+
subscriberId: firstAction.subscriberId,
|
|
378
|
+
requestedActions: activeBatch.map((action) => {
|
|
379
|
+
const telemetryAction = TelemetryAction.create({
|
|
380
|
+
operation: action.actionType,
|
|
381
|
+
callsigns: action.callsignsLookup.getCallsigns(),
|
|
382
|
+
sources: action.sources,
|
|
383
|
+
});
|
|
384
|
+
if (action.actionType === 'unsubscribe' && action.terminate) {
|
|
385
|
+
this.addTerminateToAction(telemetryAction);
|
|
386
|
+
}
|
|
387
|
+
return telemetryAction;
|
|
388
|
+
}),
|
|
285
389
|
});
|
|
286
|
-
if (actionType === 'unsubscribe' && terminate) {
|
|
287
|
-
this.addTerminateToAction(req.requestedActions[0]);
|
|
288
|
-
}
|
|
289
390
|
this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
|
|
290
391
|
// Send the message to back-end.
|
|
291
392
|
return this.requestTelemetry(req);
|
|
@@ -310,7 +411,7 @@ export class TelemetryStreamAbstract {
|
|
|
310
411
|
});
|
|
311
412
|
// Compose the request message
|
|
312
413
|
const req = TelemetryQueryRequest.create({
|
|
313
|
-
subscriberId:
|
|
414
|
+
subscriberId: firstAction.subscriberId,
|
|
314
415
|
sources: actionSources,
|
|
315
416
|
operation: actionType,
|
|
316
417
|
callsignQuery: {
|
|
@@ -37,6 +37,7 @@ export class TelemetryStreamConnect extends TelemetryStreamAbstract {
|
|
|
37
37
|
this.client = createClient(TelemetryGateway, transport);
|
|
38
38
|
}
|
|
39
39
|
stopInternal() {
|
|
40
|
+
this.subscriberId = undefined;
|
|
40
41
|
if (this.abortController) {
|
|
41
42
|
this.logger.info('Starting stop stream process');
|
|
42
43
|
this.abortController.abort();
|
|
@@ -12,6 +12,7 @@ export class TelemetryStreamNode extends TelemetryStreamAbstract {
|
|
|
12
12
|
this.credentials = config.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl();
|
|
13
13
|
}
|
|
14
14
|
stopInternal() {
|
|
15
|
+
this.subscriberId = undefined;
|
|
15
16
|
try {
|
|
16
17
|
this.logger.info('Starting stop stream process');
|
|
17
18
|
this.logger.info('Stopping stream');
|
package/esm/models/maps/Map.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SubscriberStatusEnum, } from '../models';
|
|
2
1
|
import { Subject } from 'rxjs';
|
|
3
2
|
import { BaseStreamService } from './BaseStreamService';
|
|
4
3
|
import { IDENTIFIER_NAME_SEARCH } from '../constants/identifier';
|
|
@@ -44,12 +43,8 @@ export class SearchService extends BaseStreamService {
|
|
|
44
43
|
port: this.config.port,
|
|
45
44
|
insecure: this.config.insecure,
|
|
46
45
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
|
|
50
|
-
this.status$.next(msg);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
46
|
+
// The status forwarder is wired once per stream by BaseStreamService.acquireStream's factory
|
|
47
|
+
// (and released now that the stream completes statusStream$ on stop), so no per-service one here.
|
|
53
48
|
return newStream.stream;
|
|
54
49
|
}
|
|
55
50
|
getStream(config) {
|
|
@@ -28,6 +28,10 @@ export declare class TelemetryService extends BaseStreamService<ITelemetryStream
|
|
|
28
28
|
getStatusChanges(): Observable<IStreamStatusMessage>;
|
|
29
29
|
/**
|
|
30
30
|
* Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
|
|
31
|
+
*
|
|
32
|
+
* @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
|
|
33
|
+
* normalised robot status (reported health/readiness plus platform connectivity) rather than only
|
|
34
|
+
* heartbeat-derived online/offline liveness.
|
|
31
35
|
* @param projectId
|
|
32
36
|
* @param callsign
|
|
33
37
|
* @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
|
|
@@ -85,8 +85,14 @@ export class TelemetryService extends BaseStreamService {
|
|
|
85
85
|
return from(messages$).pipe(mergeAll(), filter((message) => {
|
|
86
86
|
return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
|
|
87
87
|
}), finalize(() => {
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (params.terminateReceiverGroupOnUnsubscribe) {
|
|
89
|
+
const removal = stream.removeSubscription(subscriptionParams, true);
|
|
90
|
+
void Promise.resolve(removal).finally(release);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
release();
|
|
94
|
+
void stream.removeSubscription(subscriptionParams);
|
|
95
|
+
}
|
|
90
96
|
}));
|
|
91
97
|
});
|
|
92
98
|
}
|
|
@@ -108,6 +114,10 @@ export class TelemetryService extends BaseStreamService {
|
|
|
108
114
|
}
|
|
109
115
|
/**
|
|
110
116
|
* Subscribes to the heartbeat telemetry and checks we receive one at least every 5s
|
|
117
|
+
*
|
|
118
|
+
* @deprecated Superseded by {@link RobotStatusService.getRobotStatusChanges}, which reports the
|
|
119
|
+
* normalised robot status (reported health/readiness plus platform connectivity) rather than only
|
|
120
|
+
* heartbeat-derived online/offline liveness.
|
|
111
121
|
* @param projectId
|
|
112
122
|
* @param callsign
|
|
113
123
|
* @param heartbeatTimeoutMs optionally override the deadline for heartbeats (in ms). Default 5000ms
|