@dronedeploy/rocos-js-sdk 4.4.5 → 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 +130 -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/models/robot-status/RobotStatus.d.ts +23 -0
- package/cjs/models/robot-status/RobotStatusEnums.d.ts +18 -6
- package/cjs/models/robot-status/RobotStatusEnums.js +18 -6
- package/cjs/services/RobotStatusService.d.ts +20 -1
- package/cjs/services/RobotStatusService.js +5 -0
- package/cjs/services/SearchService.js +2 -7
- package/cjs/services/TelemetryService.d.ts +4 -1
- package/cjs/services/TelemetryService.js +17 -12
- package/cjs/services/robot-status/connectivity.d.ts +16 -12
- package/cjs/services/robot-status/connectivity.js +26 -15
- package/cjs/services/robot-status/reportedStatus.d.ts +14 -1
- package/cjs/services/robot-status/reportedStatus.js +36 -1
- 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 +131 -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/models/robot-status/RobotStatus.d.ts +23 -0
- package/esm/models/robot-status/RobotStatusEnums.d.ts +18 -6
- package/esm/models/robot-status/RobotStatusEnums.js +18 -6
- package/esm/services/RobotStatusService.d.ts +20 -1
- package/esm/services/RobotStatusService.js +6 -1
- package/esm/services/SearchService.js +2 -7
- package/esm/services/TelemetryService.d.ts +4 -1
- package/esm/services/TelemetryService.js +17 -12
- package/esm/services/robot-status/connectivity.d.ts +16 -12
- package/esm/services/robot-status/connectivity.js +26 -15
- package/esm/services/robot-status/reportedStatus.d.ts +14 -1
- package/esm/services/robot-status/reportedStatus.js +35 -1
- package/package.json +7 -2
|
@@ -36,18 +36,19 @@ export declare const buildConnectionsUrl: (serviceId: string, baseUrl: string, p
|
|
|
36
36
|
export declare const mapConnectionStatus: (connection?: IConnectedCallsign | null) => ServiceConnection;
|
|
37
37
|
/**
|
|
38
38
|
* Aggregates per-service connection states into an overall verdict:
|
|
39
|
-
* - `
|
|
40
|
-
*
|
|
39
|
+
* - `DEGRADED` — any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
|
|
40
|
+
* some disconnected).
|
|
41
|
+
* - `ONLINE` — at least one service connected and none disconnected or failed.
|
|
41
42
|
* - `OFFLINE` — nothing connected.
|
|
42
43
|
*
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
44
|
+
* A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
|
|
45
|
+
* deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
|
|
48
|
+
* (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
|
|
49
|
+
* `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
|
|
50
|
+
* aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
|
|
51
|
+
* indicator; a service that wants to be surfaced reports `FAILED` explicitly.
|
|
51
52
|
*/
|
|
52
53
|
export declare const aggregateConnectivity: (services: Record<string, ServiceConnection>) => RobotConnectivity;
|
|
53
54
|
/**
|
|
@@ -66,11 +67,14 @@ export interface ConnectivityDeps {
|
|
|
66
67
|
insecure?: boolean;
|
|
67
68
|
/** The telemetry-liveness signal (from `getTelemetryLiveness`), folded in as one service. */
|
|
68
69
|
telemetryLiveness$: Observable<ServiceConnection>;
|
|
70
|
+
/** Caller-injected connectivity signals, keyed by service id, folded in on equal footing. */
|
|
71
|
+
extraServices?: Record<string, Observable<ServiceConnection>>;
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
71
74
|
* Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
|
|
72
|
-
* plus the aggregate verdict — for the configured `expectedServices
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
+
* plus the aggregate verdict — for the configured `expectedServices`, plus any
|
|
76
|
+
* caller-injected `extraServices` (folded in on equal footing). HTTP services
|
|
77
|
+
* are polled; `telemetry-liveness` and injected services use their supplied
|
|
78
|
+
* observable; any unrecognised service id resolves to `UNKNOWN`.
|
|
75
79
|
*/
|
|
76
80
|
export declare const getConnectivityChanges: (deps: ConnectivityDeps, projectId: string, callsign: string, expectedServices?: string[], pollMs?: number) => Observable<RobotConnectivityStatus>;
|
|
@@ -58,21 +58,24 @@ const mapConnectionStatus = (connection) => {
|
|
|
58
58
|
exports.mapConnectionStatus = mapConnectionStatus;
|
|
59
59
|
/**
|
|
60
60
|
* Aggregates per-service connection states into an overall verdict:
|
|
61
|
-
* - `
|
|
62
|
-
*
|
|
61
|
+
* - `DEGRADED` — any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
|
|
62
|
+
* some disconnected).
|
|
63
|
+
* - `ONLINE` — at least one service connected and none disconnected or failed.
|
|
63
64
|
* - `OFFLINE` — nothing connected.
|
|
64
65
|
*
|
|
65
|
-
* `
|
|
66
|
-
*
|
|
66
|
+
* A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
|
|
67
|
+
* deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
|
|
67
68
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
|
|
70
|
+
* (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
|
|
71
|
+
* `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
|
|
72
|
+
* aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
|
|
73
|
+
* indicator; a service that wants to be surfaced reports `FAILED` explicitly.
|
|
73
74
|
*/
|
|
74
75
|
const aggregateConnectivity = (services) => {
|
|
75
76
|
const values = Object.values(services);
|
|
77
|
+
if (values.includes(models_1.ServiceConnection.FAILED))
|
|
78
|
+
return models_1.RobotConnectivity.DEGRADED;
|
|
76
79
|
const hasConnected = values.includes(models_1.ServiceConnection.CONNECTED);
|
|
77
80
|
const hasDisconnected = values.includes(models_1.ServiceConnection.DISCONNECTED);
|
|
78
81
|
if (hasConnected && hasDisconnected)
|
|
@@ -107,24 +110,32 @@ const connectivityEquals = (a, b) => {
|
|
|
107
110
|
};
|
|
108
111
|
/**
|
|
109
112
|
* Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
|
|
110
|
-
* plus the aggregate verdict — for the configured `expectedServices
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
+
* plus the aggregate verdict — for the configured `expectedServices`, plus any
|
|
114
|
+
* caller-injected `extraServices` (folded in on equal footing). HTTP services
|
|
115
|
+
* are polled; `telemetry-liveness` and injected services use their supplied
|
|
116
|
+
* observable; any unrecognised service id resolves to `UNKNOWN`.
|
|
113
117
|
*/
|
|
114
118
|
const getConnectivityChanges = (deps, projectId, callsign, expectedServices = exports.DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
|
|
119
|
+
const extraServices = deps.extraServices ?? {};
|
|
120
|
+
const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
|
|
115
121
|
// combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
|
|
116
|
-
if (
|
|
122
|
+
if (serviceIds.length === 0) {
|
|
117
123
|
return (0, rxjs_1.of)({ overall: models_1.RobotConnectivity.OFFLINE, services: {} });
|
|
118
124
|
}
|
|
119
|
-
const perService =
|
|
125
|
+
const perService = serviceIds.map((serviceId) => {
|
|
120
126
|
let source$;
|
|
121
127
|
if (serviceId === exports.TELEMETRY_LIVENESS_SERVICE) {
|
|
122
128
|
source$ = deps.telemetryLiveness$;
|
|
123
129
|
}
|
|
124
|
-
else if (
|
|
130
|
+
else if (Object.prototype.hasOwnProperty.call(exports.CONNECTION_SERVICE_PREFIXES, serviceId)) {
|
|
125
131
|
const url = (0, exports.buildConnectionsUrl)(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
|
|
126
132
|
source$ = (0, exports.getServiceConnectionChanges)(deps.httpGet, url, pollMs);
|
|
127
133
|
}
|
|
134
|
+
else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
|
|
135
|
+
// Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
|
|
136
|
+
// faulty caller signal can't error out combineLatest and tear down the whole aggregate.
|
|
137
|
+
source$ = extraServices[serviceId].pipe((0, rxjs_1.catchError)(() => (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN)));
|
|
138
|
+
}
|
|
128
139
|
else {
|
|
129
140
|
source$ = (0, rxjs_1.of)(models_1.ServiceConnection.UNKNOWN);
|
|
130
141
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
|
-
import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
|
|
2
|
+
import { RobotHealth, RobotReadiness, RobotStatusContributors, ServiceConnection } from '../../models';
|
|
3
3
|
import { TelemetryService } from '../TelemetryService';
|
|
4
4
|
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
5
5
|
export declare const ROBOT_STATUS_SOURCE = "/diagnostics/robot/status";
|
|
6
6
|
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
7
7
|
export declare const LEGACY_HEARTBEAT_SOURCE = "/rocos/agent/telemetry/heartbeat";
|
|
8
|
+
/** The per-contributor breakdown behind the reported status (health + readiness). */
|
|
9
|
+
export declare const ROBOT_STATUS_CONTRIBUTORS_SOURCE = "/diagnostics/robot/status/contributors";
|
|
8
10
|
export interface ReportedRobotStatus {
|
|
9
11
|
health: RobotHealth;
|
|
10
12
|
readiness: RobotReadiness;
|
|
@@ -28,6 +30,17 @@ export declare const parseRobotReadiness: (value: unknown) => RobotReadiness;
|
|
|
28
30
|
* new topic.
|
|
29
31
|
*/
|
|
30
32
|
export declare const getReportedStatusChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<ReportedRobotStatus>;
|
|
33
|
+
/**
|
|
34
|
+
* Emits the per-contributor breakdown behind the robot's reported health and
|
|
35
|
+
* readiness, from the agent's `robot/status/contributors` topic. Each
|
|
36
|
+
* contributor's integer wire value is mapped onto {@link RobotHealth} /
|
|
37
|
+
* {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
|
|
38
|
+
*
|
|
39
|
+
* Starts with an empty breakdown so consumers combining this with other streams
|
|
40
|
+
* emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
|
|
41
|
+
* error falls back to an empty breakdown rather than terminating the stream.
|
|
42
|
+
*/
|
|
43
|
+
export declare const getStatusContributorsChanges: (telemetry: TelemetryService, projectId: string, callsign: string) => Observable<RobotStatusContributors>;
|
|
31
44
|
/**
|
|
32
45
|
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
33
46
|
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getTelemetryLiveness = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
|
|
3
|
+
exports.getTelemetryLiveness = exports.getStatusContributorsChanges = exports.getReportedStatusChanges = exports.parseRobotReadiness = exports.parseRobotHealth = exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = exports.LEGACY_HEARTBEAT_SOURCE = exports.ROBOT_STATUS_SOURCE = void 0;
|
|
4
4
|
const rxjs_1 = require("rxjs");
|
|
5
5
|
const models_1 = require("../../models");
|
|
6
6
|
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
7
7
|
exports.ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
|
|
8
8
|
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
9
9
|
exports.LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
|
|
10
|
+
/** The per-contributor breakdown behind the reported status (health + readiness). */
|
|
11
|
+
exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${exports.ROBOT_STATUS_SOURCE}/contributors`;
|
|
10
12
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
|
|
11
13
|
const DEFAULT_INTERVAL_MS = 2000;
|
|
12
14
|
/**
|
|
@@ -84,6 +86,39 @@ const getReportedStatusChanges = (telemetry, projectId, callsign) => {
|
|
|
84
86
|
(0, rxjs_1.catchError)(() => (0, rxjs_1.of)({ health: models_1.RobotHealth.UNKNOWN, readiness: models_1.RobotReadiness.UNKNOWN })));
|
|
85
87
|
};
|
|
86
88
|
exports.getReportedStatusChanges = getReportedStatusChanges;
|
|
89
|
+
const toContributor = (raw, parse) => ({
|
|
90
|
+
id: raw.id ?? '',
|
|
91
|
+
component: raw.component ?? '',
|
|
92
|
+
reason: raw.reason ?? '',
|
|
93
|
+
status: parse(raw.value),
|
|
94
|
+
});
|
|
95
|
+
const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
|
|
96
|
+
/**
|
|
97
|
+
* Emits the per-contributor breakdown behind the robot's reported health and
|
|
98
|
+
* readiness, from the agent's `robot/status/contributors` topic. Each
|
|
99
|
+
* contributor's integer wire value is mapped onto {@link RobotHealth} /
|
|
100
|
+
* {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
|
|
101
|
+
*
|
|
102
|
+
* Starts with an empty breakdown so consumers combining this with other streams
|
|
103
|
+
* emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
|
|
104
|
+
* error falls back to an empty breakdown rather than terminating the stream.
|
|
105
|
+
*/
|
|
106
|
+
const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
|
|
107
|
+
return telemetry
|
|
108
|
+
.subscribe({
|
|
109
|
+
projectId,
|
|
110
|
+
callsigns: [callsign],
|
|
111
|
+
sources: [exports.ROBOT_STATUS_CONTRIBUTORS_SOURCE],
|
|
112
|
+
})
|
|
113
|
+
.pipe((0, rxjs_1.map)((message) => {
|
|
114
|
+
const payload = message.payload ?? {};
|
|
115
|
+
return {
|
|
116
|
+
health: (payload.health ?? []).map((c) => toContributor(c, exports.parseRobotHealth)),
|
|
117
|
+
readiness: (payload.readiness ?? []).map((c) => toContributor(c, exports.parseRobotReadiness)),
|
|
118
|
+
};
|
|
119
|
+
}), (0, rxjs_1.startWith)(EMPTY_CONTRIBUTORS), (0, rxjs_1.distinctUntilChanged)((a, b) => JSON.stringify(a) === JSON.stringify(b)), (0, rxjs_1.catchError)(() => (0, rxjs_1.of)(EMPTY_CONTRIBUTORS)));
|
|
120
|
+
};
|
|
121
|
+
exports.getStatusContributorsChanges = getStatusContributorsChanges;
|
|
87
122
|
/**
|
|
88
123
|
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
89
124
|
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
|
@@ -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) {
|
|
@@ -59,6 +100,10 @@ export class TelemetryStreamAbstract {
|
|
|
59
100
|
}
|
|
60
101
|
this.subscriberStatus = SubscriberStatusEnum.STOPPED;
|
|
61
102
|
this.statusStream$.next(this.subscriberStatus);
|
|
103
|
+
// The stream is being torn down (the registry only stops a stream when it is evicted), so
|
|
104
|
+
// complete the status stream: this releases every service-level forwarder subscribed to it
|
|
105
|
+
// rather than leaving them attached to a dead instance.
|
|
106
|
+
this.statusStream$.complete();
|
|
62
107
|
}
|
|
63
108
|
addSubscription(params) {
|
|
64
109
|
this.logger.info('Adding subscriptions from stream', params.uniqueId);
|
|
@@ -80,7 +125,9 @@ export class TelemetryStreamAbstract {
|
|
|
80
125
|
const toAdd = arrayRemove(this.sources, sourcesBefore);
|
|
81
126
|
if (toAdd.length) {
|
|
82
127
|
this.logger.debug('New query subscriptions added, we need to refresh sources', { toAdd });
|
|
83
|
-
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
|
+
});
|
|
84
131
|
}
|
|
85
132
|
}
|
|
86
133
|
else {
|
|
@@ -91,7 +138,9 @@ export class TelemetryStreamAbstract {
|
|
|
91
138
|
const { toAdd } = getSubscriptionsDifference(before, after);
|
|
92
139
|
if (toAdd.callsigns.length && toAdd.sources.length) {
|
|
93
140
|
this.logger.debug('New subscriptions added, we need to refresh sources', { toAdd });
|
|
94
|
-
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
|
+
});
|
|
95
144
|
}
|
|
96
145
|
}
|
|
97
146
|
}
|
|
@@ -197,14 +246,22 @@ export class TelemetryStreamAbstract {
|
|
|
197
246
|
// Subscribe message
|
|
198
247
|
case GRPC_SOURCE_SUBSCRIBED: {
|
|
199
248
|
const { subscriberId } = rocosMessage.payload;
|
|
249
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
250
|
+
if (queueState?.stopped)
|
|
251
|
+
return;
|
|
200
252
|
if (this.subscriberId && subscriberId) {
|
|
201
253
|
this.logger.warn(`Replacing old subscriberId (${this.subscriberId}) with a new one (${subscriberId})`);
|
|
202
254
|
}
|
|
203
|
-
|
|
255
|
+
if (queueState) {
|
|
256
|
+
queueState.registration++;
|
|
257
|
+
resolveUnsentTelemetryActions(queueState);
|
|
258
|
+
}
|
|
204
259
|
this.subscriberId = subscriberId;
|
|
205
260
|
rocosMessage.subscriberId = this.subscriberId;
|
|
206
261
|
this.logger.info('onData', `subscriberId has been updated - ${subscriberId}`, { subscriberId });
|
|
207
|
-
void this.enqueueTelemetryAction('subscribe')
|
|
262
|
+
void this.enqueueTelemetryAction('subscribe').catch((error) => {
|
|
263
|
+
this.logger.error(`Failed to subscribe: ${error}`);
|
|
264
|
+
});
|
|
208
265
|
this.heartbeat.start(() => this.sendHeartbeat(), this.sendHeartbeatTime);
|
|
209
266
|
isRobotMessage = false;
|
|
210
267
|
break;
|
|
@@ -246,21 +303,69 @@ export class TelemetryStreamAbstract {
|
|
|
246
303
|
* unsubscribe landing last strands a subscribe and nothing re-adds it.
|
|
247
304
|
*/
|
|
248
305
|
enqueueTelemetryAction(actionType, callsignsLookup, sources, terminate) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
355
|
}
|
|
256
|
-
async takeTelemetryAction(actionType, callsignsLookup, sources
|
|
356
|
+
async takeTelemetryAction(actionType, callsignsLookup, sources) {
|
|
257
357
|
this.logger.debug('takeTelemetryAction', actionType);
|
|
358
|
+
const queueState = telemetryActionQueueStates.get(this);
|
|
359
|
+
const activeBatch = queueState?.activeBatch ?? [];
|
|
360
|
+
const [firstAction] = activeBatch;
|
|
258
361
|
// A config fault, not a race: this stream can never address a request.
|
|
259
362
|
if (!this.projectId) {
|
|
260
363
|
this.logger.warn('takeTelemetryAction', actionType, `${actionType} without a projectId - dropped`);
|
|
261
364
|
return Promise.resolve();
|
|
262
365
|
}
|
|
263
|
-
if (!
|
|
366
|
+
if (!firstAction?.subscriberId ||
|
|
367
|
+
firstAction.subscriberId !== this.subscriberId ||
|
|
368
|
+
firstAction.registration !== queueState?.registration) {
|
|
264
369
|
// Deferred, not lost: `onData` re-asserts `this.sources` when the subscriberId arrives.
|
|
265
370
|
this.logger.debug('takeTelemetryAction', actionType, `${actionType} before the receiver was registered - deferred to the post-registration re-assert`);
|
|
266
371
|
return Promise.resolve();
|
|
@@ -268,20 +373,20 @@ export class TelemetryStreamAbstract {
|
|
|
268
373
|
const actionSources = sources ?? this.sources;
|
|
269
374
|
const actionCallsigns = callsignsLookup ?? this.callsignsLookup;
|
|
270
375
|
if (!actionCallsigns.isQuery()) {
|
|
271
|
-
// Compose the request message
|
|
272
376
|
const req = TelemetryRequest.create({
|
|
273
|
-
subscriberId:
|
|
274
|
-
requestedActions:
|
|
275
|
-
{
|
|
276
|
-
operation: actionType,
|
|
277
|
-
callsigns:
|
|
278
|
-
sources:
|
|
279
|
-
}
|
|
280
|
-
|
|
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
|
+
}),
|
|
281
389
|
});
|
|
282
|
-
if (actionType === 'unsubscribe' && terminate) {
|
|
283
|
-
this.addTerminateToAction(req.requestedActions[0]);
|
|
284
|
-
}
|
|
285
390
|
this.logger.debug('takeTelemetryAction', actionType, 'client.requestTelemetry', this.subscriberId);
|
|
286
391
|
// Send the message to back-end.
|
|
287
392
|
return this.requestTelemetry(req);
|
|
@@ -306,7 +411,7 @@ export class TelemetryStreamAbstract {
|
|
|
306
411
|
});
|
|
307
412
|
// Compose the request message
|
|
308
413
|
const req = TelemetryQueryRequest.create({
|
|
309
|
-
subscriberId:
|
|
414
|
+
subscriberId: firstAction.subscriberId,
|
|
310
415
|
sources: actionSources,
|
|
311
416
|
operation: actionType,
|
|
312
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
|
@@ -41,3 +41,26 @@ export interface RobotStatus {
|
|
|
41
41
|
connectivity: RobotConnectivityStatus;
|
|
42
42
|
colour: RobotStatusColour;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* One contributor to a reported-status axis, from the agent's
|
|
46
|
+
* `robot/status/contributors` topic. `status` is the contributor's own verdict
|
|
47
|
+
* for that axis — the integer wire value mapped onto the enum.
|
|
48
|
+
*/
|
|
49
|
+
export interface RobotStatusContributor<TStatus = RobotHealth | RobotReadiness> {
|
|
50
|
+
/** Identifier of the contributor, e.g. `battery`. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** The agent component that raised it, e.g. `dji`. */
|
|
53
|
+
component: string;
|
|
54
|
+
/** Human-readable explanation, e.g. `battery below 20%`; empty when none was given. */
|
|
55
|
+
reason: string;
|
|
56
|
+
/** The contributor's verdict for its axis. */
|
|
57
|
+
status: TStatus;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The per-contributor breakdown behind a robot's reported health and readiness,
|
|
61
|
+
* from the agent's `robot/status/contributors` topic.
|
|
62
|
+
*/
|
|
63
|
+
export interface RobotStatusContributors {
|
|
64
|
+
health: RobotStatusContributor<RobotHealth>[];
|
|
65
|
+
readiness: RobotStatusContributor<RobotReadiness>[];
|
|
66
|
+
}
|