@dronedeploy/rocos-js-sdk 4.4.5 → 4.4.6
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/streams/telemetry/TelemetryStreamAbstract.js +4 -0
- 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/TelemetryService.d.ts +0 -1
- package/cjs/services/TelemetryService.js +5 -10
- 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/streams/telemetry/TelemetryStreamAbstract.js +4 -0
- 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/TelemetryService.d.ts +0 -1
- package/esm/services/TelemetryService.js +5 -10
- 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
|
@@ -62,6 +62,10 @@ class TelemetryStreamAbstract {
|
|
|
62
62
|
}
|
|
63
63
|
this.subscriberStatus = models_1.SubscriberStatusEnum.STOPPED;
|
|
64
64
|
this.statusStream$.next(this.subscriberStatus);
|
|
65
|
+
// The stream is being torn down (the registry only stops a stream when it is evicted), so
|
|
66
|
+
// complete the status stream: this releases every service-level forwarder subscribed to it
|
|
67
|
+
// rather than leaving them attached to a dead instance.
|
|
68
|
+
this.statusStream$.complete();
|
|
65
69
|
}
|
|
66
70
|
addSubscription(params) {
|
|
67
71
|
this.logger.info('Adding subscriptions from stream', params.uniqueId);
|
|
@@ -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
|
+
}
|
|
@@ -30,13 +30,22 @@ export declare enum RobotReadiness {
|
|
|
30
30
|
BUSY = "busy"
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Whether the robot is connected to a single cloud service
|
|
34
|
-
* service's `connections` endpoint
|
|
35
|
-
*
|
|
33
|
+
* Whether the robot is connected to a single cloud service — reported by that
|
|
34
|
+
* service's `connections` endpoint, derived from heartbeat freshness (for
|
|
35
|
+
* `telemetry-liveness`), or supplied by a caller-injected signal
|
|
36
|
+
* (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
|
|
36
37
|
*/
|
|
37
38
|
export declare enum ServiceConnection {
|
|
38
39
|
CONNECTED = "connected",
|
|
39
40
|
DISCONNECTED = "disconnected",
|
|
41
|
+
/**
|
|
42
|
+
* The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
|
|
43
|
+
* means "not connected", as expected of a robot that is simply off). A failed virtual-robot
|
|
44
|
+
* deployment is the motivating case. Any `FAILED` service forces the aggregate to
|
|
45
|
+
* {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
|
|
46
|
+
* connected — see `aggregateConnectivity`.
|
|
47
|
+
*/
|
|
48
|
+
FAILED = "failed",
|
|
40
49
|
/** The service's connectivity has not yet been determined (e.g. not polled). */
|
|
41
50
|
UNKNOWN = "unknown"
|
|
42
51
|
}
|
|
@@ -45,10 +54,13 @@ export declare enum ServiceConnection {
|
|
|
45
54
|
* expected services.
|
|
46
55
|
*/
|
|
47
56
|
export declare enum RobotConnectivity {
|
|
48
|
-
/** At least one expected service is connected and none is disconnected. */
|
|
57
|
+
/** At least one expected service is connected and none is disconnected or failed. */
|
|
49
58
|
ONLINE = "online",
|
|
50
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
|
|
61
|
+
* connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
|
|
62
|
+
*/
|
|
51
63
|
DEGRADED = "degraded",
|
|
52
|
-
/**
|
|
64
|
+
/** Nothing connected, and nothing failed. */
|
|
53
65
|
OFFLINE = "offline"
|
|
54
66
|
}
|
|
@@ -35,14 +35,23 @@ var RobotReadiness;
|
|
|
35
35
|
RobotReadiness["BUSY"] = "busy";
|
|
36
36
|
})(RobotReadiness || (exports.RobotReadiness = RobotReadiness = {}));
|
|
37
37
|
/**
|
|
38
|
-
* Whether the robot is connected to a single cloud service
|
|
39
|
-
* service's `connections` endpoint
|
|
40
|
-
*
|
|
38
|
+
* Whether the robot is connected to a single cloud service — reported by that
|
|
39
|
+
* service's `connections` endpoint, derived from heartbeat freshness (for
|
|
40
|
+
* `telemetry-liveness`), or supplied by a caller-injected signal
|
|
41
|
+
* (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
|
|
41
42
|
*/
|
|
42
43
|
var ServiceConnection;
|
|
43
44
|
(function (ServiceConnection) {
|
|
44
45
|
ServiceConnection["CONNECTED"] = "connected";
|
|
45
46
|
ServiceConnection["DISCONNECTED"] = "disconnected";
|
|
47
|
+
/**
|
|
48
|
+
* The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
|
|
49
|
+
* means "not connected", as expected of a robot that is simply off). A failed virtual-robot
|
|
50
|
+
* deployment is the motivating case. Any `FAILED` service forces the aggregate to
|
|
51
|
+
* {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
|
|
52
|
+
* connected — see `aggregateConnectivity`.
|
|
53
|
+
*/
|
|
54
|
+
ServiceConnection["FAILED"] = "failed";
|
|
46
55
|
/** The service's connectivity has not yet been determined (e.g. not polled). */
|
|
47
56
|
ServiceConnection["UNKNOWN"] = "unknown";
|
|
48
57
|
})(ServiceConnection || (exports.ServiceConnection = ServiceConnection = {}));
|
|
@@ -52,10 +61,13 @@ var ServiceConnection;
|
|
|
52
61
|
*/
|
|
53
62
|
var RobotConnectivity;
|
|
54
63
|
(function (RobotConnectivity) {
|
|
55
|
-
/** At least one expected service is connected and none is disconnected. */
|
|
64
|
+
/** At least one expected service is connected and none is disconnected or failed. */
|
|
56
65
|
RobotConnectivity["ONLINE"] = "online";
|
|
57
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
|
|
68
|
+
* connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
|
|
69
|
+
*/
|
|
58
70
|
RobotConnectivity["DEGRADED"] = "degraded";
|
|
59
|
-
/**
|
|
71
|
+
/** Nothing connected, and nothing failed. */
|
|
60
72
|
RobotConnectivity["OFFLINE"] = "offline";
|
|
61
73
|
})(RobotConnectivity || (exports.RobotConnectivity = RobotConnectivity = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
|
|
1
|
+
import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RobotStatusContributors, RocosError, ServiceConnection } from '../models';
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
3
|
import { ReportedRobotStatus } from './robot-status/reportedStatus';
|
|
4
4
|
import { BaseServiceAbstract } from './BaseServiceAbstract';
|
|
@@ -10,6 +10,23 @@ export interface RobotStatusOptions {
|
|
|
10
10
|
heartbeatTimeoutMs?: number;
|
|
11
11
|
/** How often the connections endpoints are polled (ms, floored internally). */
|
|
12
12
|
connectivityPollMs?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Extra per-robot connectivity signals, keyed by service id, folded into
|
|
15
|
+
* {@link RobotConnectivityStatus.services} and the aggregate on equal footing with the built-in
|
|
16
|
+
* services. Lets a caller inject a signal the SDK can't derive itself (e.g. virtual-robot
|
|
17
|
+
* deployment status); emit {@link ServiceConnection.FAILED} to surface a fault as `DEGRADED`.
|
|
18
|
+
*
|
|
19
|
+
* A key that collides with a built-in service id (`telemetry-liveness`, `robot-configs`,
|
|
20
|
+
* `robot-services`) is ignored in favour of the built-in.
|
|
21
|
+
*
|
|
22
|
+
* An error from an injected observable is contained as {@link ServiceConnection.UNKNOWN} so it
|
|
23
|
+
* can't tear down the overall status stream — but it is *terminal* for that signal: unlike the
|
|
24
|
+
* built-in HTTP poller (which recovers on its next tick), the errored observable is not
|
|
25
|
+
* resubscribed, so the service stays `UNKNOWN` for the life of the stream (and `getConnectivity`
|
|
26
|
+
* then waits out its timeout). To report a recoverable fault, emit `FAILED` / `UNKNOWN` as a
|
|
27
|
+
* value rather than letting the observable error.
|
|
28
|
+
*/
|
|
29
|
+
extraServices?: Record<string, Observable<ServiceConnection>>;
|
|
13
30
|
}
|
|
14
31
|
/**
|
|
15
32
|
* The single source of truth for a robot's status. It composes the reported
|
|
@@ -22,6 +39,8 @@ export declare class RobotStatusService extends BaseServiceAbstract implements I
|
|
|
22
39
|
getStatus(): boolean;
|
|
23
40
|
/** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
|
|
24
41
|
getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
|
|
42
|
+
/** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
|
|
43
|
+
getStatusContributorsChanges(projectId: string, callsign: string): Observable<RobotStatusContributors>;
|
|
25
44
|
/** Per-service connectivity plus the aggregate verdict, polled. */
|
|
26
45
|
getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
|
|
27
46
|
/** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
|
|
@@ -42,6 +42,10 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
|
|
|
42
42
|
getReportedStatusChanges(projectId, callsign) {
|
|
43
43
|
return (0, reportedStatus_1.getReportedStatusChanges)(this.telemetryService, projectId, callsign);
|
|
44
44
|
}
|
|
45
|
+
/** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
|
|
46
|
+
getStatusContributorsChanges(projectId, callsign) {
|
|
47
|
+
return (0, reportedStatus_1.getStatusContributorsChanges)(this.telemetryService, projectId, callsign);
|
|
48
|
+
}
|
|
45
49
|
/** Per-service connectivity plus the aggregate verdict, polled. */
|
|
46
50
|
getConnectivityChanges(projectId, callsign, opts) {
|
|
47
51
|
return (0, connectivity_1.getConnectivityChanges)(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
|
|
@@ -88,6 +92,7 @@ class RobotStatusService extends BaseServiceAbstract_1.BaseServiceAbstract {
|
|
|
88
92
|
baseUrl: this.config.url,
|
|
89
93
|
insecure: this.config.insecure,
|
|
90
94
|
telemetryLiveness$: (0, reportedStatus_1.getTelemetryLiveness)(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
|
|
95
|
+
extraServices: opts?.extraServices,
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
}
|
|
@@ -3,7 +3,6 @@ import { CallsignStatus, IRocosSDKConfig, IRocosTelemetryMessage, IStreamStatusM
|
|
|
3
3
|
import { BaseStreamService } from './BaseStreamService';
|
|
4
4
|
export declare class TelemetryService extends BaseStreamService<ITelemetryStream, ITelemetryStreamConfig> {
|
|
5
5
|
private static grpcWebDeprecationWarned;
|
|
6
|
-
private statusSubscription;
|
|
7
6
|
private changeStream$;
|
|
8
7
|
private streamStatusSubject$;
|
|
9
8
|
constructor(config: IRocosSDKConfig);
|
|
@@ -67,7 +67,9 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
|
|
|
67
67
|
const { stream, isNew, ready, release } = this.acquireStream(identifier_1.IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
|
|
68
68
|
try {
|
|
69
69
|
stream.addSubscription(subscriptionParams);
|
|
70
|
-
|
|
70
|
+
// Forward the stream's status onto the per-scope subject once, at creation; it ends when the
|
|
71
|
+
// stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
|
|
72
|
+
if (isNew) {
|
|
71
73
|
stream.statusStream$.subscribe((msg) => {
|
|
72
74
|
this.streamStatusSubject$.next({
|
|
73
75
|
scope,
|
|
@@ -81,15 +83,8 @@ class TelemetryService extends BaseStreamService_1.BaseStreamService {
|
|
|
81
83
|
release();
|
|
82
84
|
throw e;
|
|
83
85
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.statusSubscription = stream.statusStream$.subscribe((msg) => {
|
|
87
|
-
this.status = msg === models_1.SubscriberStatusEnum.STOPPED || msg === models_1.SubscriberStatusEnum.ALIVE;
|
|
88
|
-
this.status$.next(msg);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
return (0, rxjs_1.from)(stream.messageStream$);
|
|
92
|
-
});
|
|
86
|
+
// status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
|
|
87
|
+
const messages$ = ready.then(() => (0, rxjs_1.from)(stream.messageStream$));
|
|
93
88
|
return (0, rxjs_1.from)(messages$).pipe((0, rxjs_1.mergeAll)(), (0, operators_1.filter)((message) => {
|
|
94
89
|
return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
|
|
95
90
|
}), (0, operators_1.finalize)(() => {
|
|
@@ -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
|
|
@@ -59,6 +59,10 @@ export class TelemetryStreamAbstract {
|
|
|
59
59
|
}
|
|
60
60
|
this.subscriberStatus = SubscriberStatusEnum.STOPPED;
|
|
61
61
|
this.statusStream$.next(this.subscriberStatus);
|
|
62
|
+
// The stream is being torn down (the registry only stops a stream when it is evicted), so
|
|
63
|
+
// complete the status stream: this releases every service-level forwarder subscribed to it
|
|
64
|
+
// rather than leaving them attached to a dead instance.
|
|
65
|
+
this.statusStream$.complete();
|
|
62
66
|
}
|
|
63
67
|
addSubscription(params) {
|
|
64
68
|
this.logger.info('Adding subscriptions from stream', params.uniqueId);
|
|
@@ -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
|
+
}
|
|
@@ -30,13 +30,22 @@ export declare enum RobotReadiness {
|
|
|
30
30
|
BUSY = "busy"
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Whether the robot is connected to a single cloud service
|
|
34
|
-
* service's `connections` endpoint
|
|
35
|
-
*
|
|
33
|
+
* Whether the robot is connected to a single cloud service — reported by that
|
|
34
|
+
* service's `connections` endpoint, derived from heartbeat freshness (for
|
|
35
|
+
* `telemetry-liveness`), or supplied by a caller-injected signal
|
|
36
|
+
* (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
|
|
36
37
|
*/
|
|
37
38
|
export declare enum ServiceConnection {
|
|
38
39
|
CONNECTED = "connected",
|
|
39
40
|
DISCONNECTED = "disconnected",
|
|
41
|
+
/**
|
|
42
|
+
* The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
|
|
43
|
+
* means "not connected", as expected of a robot that is simply off). A failed virtual-robot
|
|
44
|
+
* deployment is the motivating case. Any `FAILED` service forces the aggregate to
|
|
45
|
+
* {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
|
|
46
|
+
* connected — see `aggregateConnectivity`.
|
|
47
|
+
*/
|
|
48
|
+
FAILED = "failed",
|
|
40
49
|
/** The service's connectivity has not yet been determined (e.g. not polled). */
|
|
41
50
|
UNKNOWN = "unknown"
|
|
42
51
|
}
|
|
@@ -45,10 +54,13 @@ export declare enum ServiceConnection {
|
|
|
45
54
|
* expected services.
|
|
46
55
|
*/
|
|
47
56
|
export declare enum RobotConnectivity {
|
|
48
|
-
/** At least one expected service is connected and none is disconnected. */
|
|
57
|
+
/** At least one expected service is connected and none is disconnected or failed. */
|
|
49
58
|
ONLINE = "online",
|
|
50
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
|
|
61
|
+
* connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
|
|
62
|
+
*/
|
|
51
63
|
DEGRADED = "degraded",
|
|
52
|
-
/**
|
|
64
|
+
/** Nothing connected, and nothing failed. */
|
|
53
65
|
OFFLINE = "offline"
|
|
54
66
|
}
|
|
@@ -32,14 +32,23 @@ export var RobotReadiness;
|
|
|
32
32
|
RobotReadiness["BUSY"] = "busy";
|
|
33
33
|
})(RobotReadiness || (RobotReadiness = {}));
|
|
34
34
|
/**
|
|
35
|
-
* Whether the robot is connected to a single cloud service
|
|
36
|
-
* service's `connections` endpoint
|
|
37
|
-
*
|
|
35
|
+
* Whether the robot is connected to a single cloud service — reported by that
|
|
36
|
+
* service's `connections` endpoint, derived from heartbeat freshness (for
|
|
37
|
+
* `telemetry-liveness`), or supplied by a caller-injected signal
|
|
38
|
+
* (`RobotStatusOptions.extraServices`), the only source that can report {@link FAILED}.
|
|
38
39
|
*/
|
|
39
40
|
export var ServiceConnection;
|
|
40
41
|
(function (ServiceConnection) {
|
|
41
42
|
ServiceConnection["CONNECTED"] = "connected";
|
|
42
43
|
ServiceConnection["DISCONNECTED"] = "disconnected";
|
|
44
|
+
/**
|
|
45
|
+
* The service reports an explicit fault — a stronger signal than {@link DISCONNECTED} (which just
|
|
46
|
+
* means "not connected", as expected of a robot that is simply off). A failed virtual-robot
|
|
47
|
+
* deployment is the motivating case. Any `FAILED` service forces the aggregate to
|
|
48
|
+
* {@link RobotConnectivity.DEGRADED}, so a fault raises attention even when nothing else is
|
|
49
|
+
* connected — see `aggregateConnectivity`.
|
|
50
|
+
*/
|
|
51
|
+
ServiceConnection["FAILED"] = "failed";
|
|
43
52
|
/** The service's connectivity has not yet been determined (e.g. not polled). */
|
|
44
53
|
ServiceConnection["UNKNOWN"] = "unknown";
|
|
45
54
|
})(ServiceConnection || (ServiceConnection = {}));
|
|
@@ -49,10 +58,13 @@ export var ServiceConnection;
|
|
|
49
58
|
*/
|
|
50
59
|
export var RobotConnectivity;
|
|
51
60
|
(function (RobotConnectivity) {
|
|
52
|
-
/** At least one expected service is connected and none is disconnected. */
|
|
61
|
+
/** At least one expected service is connected and none is disconnected or failed. */
|
|
53
62
|
RobotConnectivity["ONLINE"] = "online";
|
|
54
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Any service reports a fault ({@link ServiceConnection.FAILED}), or a genuine mix — some
|
|
65
|
+
* connected, some disconnected. A `FAILED` service forces this even when nothing is connected.
|
|
66
|
+
*/
|
|
55
67
|
RobotConnectivity["DEGRADED"] = "degraded";
|
|
56
|
-
/**
|
|
68
|
+
/** Nothing connected, and nothing failed. */
|
|
57
69
|
RobotConnectivity["OFFLINE"] = "offline";
|
|
58
70
|
})(RobotConnectivity || (RobotConnectivity = {}));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RocosError } from '../models';
|
|
1
|
+
import { IBaseService, IRocosSDKConfig, RobotConnectivityStatus, RobotStatus, RobotStatusContributors, RocosError, ServiceConnection } from '../models';
|
|
2
2
|
import { Observable } from 'rxjs';
|
|
3
3
|
import { ReportedRobotStatus } from './robot-status/reportedStatus';
|
|
4
4
|
import { BaseServiceAbstract } from './BaseServiceAbstract';
|
|
@@ -10,6 +10,23 @@ export interface RobotStatusOptions {
|
|
|
10
10
|
heartbeatTimeoutMs?: number;
|
|
11
11
|
/** How often the connections endpoints are polled (ms, floored internally). */
|
|
12
12
|
connectivityPollMs?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Extra per-robot connectivity signals, keyed by service id, folded into
|
|
15
|
+
* {@link RobotConnectivityStatus.services} and the aggregate on equal footing with the built-in
|
|
16
|
+
* services. Lets a caller inject a signal the SDK can't derive itself (e.g. virtual-robot
|
|
17
|
+
* deployment status); emit {@link ServiceConnection.FAILED} to surface a fault as `DEGRADED`.
|
|
18
|
+
*
|
|
19
|
+
* A key that collides with a built-in service id (`telemetry-liveness`, `robot-configs`,
|
|
20
|
+
* `robot-services`) is ignored in favour of the built-in.
|
|
21
|
+
*
|
|
22
|
+
* An error from an injected observable is contained as {@link ServiceConnection.UNKNOWN} so it
|
|
23
|
+
* can't tear down the overall status stream — but it is *terminal* for that signal: unlike the
|
|
24
|
+
* built-in HTTP poller (which recovers on its next tick), the errored observable is not
|
|
25
|
+
* resubscribed, so the service stays `UNKNOWN` for the life of the stream (and `getConnectivity`
|
|
26
|
+
* then waits out its timeout). To report a recoverable fault, emit `FAILED` / `UNKNOWN` as a
|
|
27
|
+
* value rather than letting the observable error.
|
|
28
|
+
*/
|
|
29
|
+
extraServices?: Record<string, Observable<ServiceConnection>>;
|
|
13
30
|
}
|
|
14
31
|
/**
|
|
15
32
|
* The single source of truth for a robot's status. It composes the reported
|
|
@@ -22,6 +39,8 @@ export declare class RobotStatusService extends BaseServiceAbstract implements I
|
|
|
22
39
|
getStatus(): boolean;
|
|
23
40
|
/** Health + readiness as reported by the robot (new topic, legacy heartbeat fallback). */
|
|
24
41
|
getReportedStatusChanges(projectId: string, callsign: string): Observable<ReportedRobotStatus>;
|
|
42
|
+
/** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
|
|
43
|
+
getStatusContributorsChanges(projectId: string, callsign: string): Observable<RobotStatusContributors>;
|
|
25
44
|
/** Per-service connectivity plus the aggregate verdict, polled. */
|
|
26
45
|
getConnectivityChanges(projectId: string, callsign: string, opts?: RobotStatusOptions): Observable<RobotConnectivityStatus>;
|
|
27
46
|
/** One-shot connectivity snapshot: resolves once every expected service is known, or after a timeout. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getConnectivityChanges } from './robot-status/connectivity';
|
|
2
2
|
import { RobotHealth, RobotReadiness, RocosError, ServiceConnection, errorCodes, } from '../models';
|
|
3
3
|
import { combineLatest, distinctUntilChanged, first, firstValueFrom, map, of, shareReplay, startWith, take, tap, timeout, } from 'rxjs';
|
|
4
|
-
import { getReportedStatusChanges, getTelemetryLiveness } from './robot-status/reportedStatus';
|
|
4
|
+
import { getReportedStatusChanges, getStatusContributorsChanges, getTelemetryLiveness, } from './robot-status/reportedStatus';
|
|
5
5
|
import { BaseServiceAbstract } from './BaseServiceAbstract';
|
|
6
6
|
import { RocosLogger } from '../logger/RocosLogger';
|
|
7
7
|
import { RocosStore } from '../store/RocosStore';
|
|
@@ -39,6 +39,10 @@ export class RobotStatusService extends BaseServiceAbstract {
|
|
|
39
39
|
getReportedStatusChanges(projectId, callsign) {
|
|
40
40
|
return getReportedStatusChanges(this.telemetryService, projectId, callsign);
|
|
41
41
|
}
|
|
42
|
+
/** Per-contributor breakdown behind the reported health and readiness, with the reason for each. */
|
|
43
|
+
getStatusContributorsChanges(projectId, callsign) {
|
|
44
|
+
return getStatusContributorsChanges(this.telemetryService, projectId, callsign);
|
|
45
|
+
}
|
|
42
46
|
/** Per-service connectivity plus the aggregate verdict, polled. */
|
|
43
47
|
getConnectivityChanges(projectId, callsign, opts) {
|
|
44
48
|
return getConnectivityChanges(this.connectivityDeps(projectId, callsign, opts), projectId, callsign, opts?.expectedServices, opts?.connectivityPollMs);
|
|
@@ -85,6 +89,7 @@ export class RobotStatusService extends BaseServiceAbstract {
|
|
|
85
89
|
baseUrl: this.config.url,
|
|
86
90
|
insecure: this.config.insecure,
|
|
87
91
|
telemetryLiveness$: getTelemetryLiveness(this.telemetryService, projectId, callsign, opts?.heartbeatTimeoutMs),
|
|
92
|
+
extraServices: opts?.extraServices,
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -3,7 +3,6 @@ import { CallsignStatus, IRocosSDKConfig, IRocosTelemetryMessage, IStreamStatusM
|
|
|
3
3
|
import { BaseStreamService } from './BaseStreamService';
|
|
4
4
|
export declare class TelemetryService extends BaseStreamService<ITelemetryStream, ITelemetryStreamConfig> {
|
|
5
5
|
private static grpcWebDeprecationWarned;
|
|
6
|
-
private statusSubscription;
|
|
7
6
|
private changeStream$;
|
|
8
7
|
private streamStatusSubject$;
|
|
9
8
|
constructor(config: IRocosSDKConfig);
|
|
@@ -64,7 +64,9 @@ export class TelemetryService extends BaseStreamService {
|
|
|
64
64
|
const { stream, isNew, ready, release } = this.acquireStream(IDENTIFIER_NAME_TELEMETRY, this.buildStreamConfig(params.projectId, callsignsLookup, sources, scope));
|
|
65
65
|
try {
|
|
66
66
|
stream.addSubscription(subscriptionParams);
|
|
67
|
-
|
|
67
|
+
// Forward the stream's status onto the per-scope subject once, at creation; it ends when the
|
|
68
|
+
// stream stops (statusStream$ completes), so it is not re-created or leaked across subscribers.
|
|
69
|
+
if (isNew) {
|
|
68
70
|
stream.statusStream$.subscribe((msg) => {
|
|
69
71
|
this.streamStatusSubject$.next({
|
|
70
72
|
scope,
|
|
@@ -78,15 +80,8 @@ export class TelemetryService extends BaseStreamService {
|
|
|
78
80
|
release();
|
|
79
81
|
throw e;
|
|
80
82
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.statusSubscription = stream.statusStream$.subscribe((msg) => {
|
|
84
|
-
this.status = msg === SubscriberStatusEnum.STOPPED || msg === SubscriberStatusEnum.ALIVE;
|
|
85
|
-
this.status$.next(msg);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return from(stream.messageStream$);
|
|
89
|
-
});
|
|
83
|
+
// status$ / getStatus() are driven by the acquireStream factory (BaseStreamService), not here.
|
|
84
|
+
const messages$ = ready.then(() => from(stream.messageStream$));
|
|
90
85
|
return from(messages$).pipe(mergeAll(), filter((message) => {
|
|
91
86
|
return callsignsLookup.includesCallsign(message.callsign) && sources.includes(message.source);
|
|
92
87
|
}), finalize(() => {
|
|
@@ -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>;
|
|
@@ -52,21 +52,24 @@ export const mapConnectionStatus = (connection) => {
|
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
54
|
* Aggregates per-service connection states into an overall verdict:
|
|
55
|
-
* - `
|
|
56
|
-
*
|
|
55
|
+
* - `DEGRADED` — any service reports an explicit `FAILED` fault, or a genuine mix (some connected,
|
|
56
|
+
* some disconnected).
|
|
57
|
+
* - `ONLINE` — at least one service connected and none disconnected or failed.
|
|
57
58
|
* - `OFFLINE` — nothing connected.
|
|
58
59
|
*
|
|
59
|
-
* `
|
|
60
|
-
*
|
|
60
|
+
* A `FAILED` service forces `DEGRADED` even when nothing else is connected, so a fault (e.g. a failed
|
|
61
|
+
* deployment) raises attention rather than reading as a plain, switched-off `OFFLINE`.
|
|
61
62
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
63
|
+
* `UNKNOWN` services (e.g. not yet polled) never force `DEGRADED`, so startup settles on `OFFLINE`
|
|
64
|
+
* (gray) rather than a spurious flashing-red degraded state. This optimism also means a service held
|
|
65
|
+
* `UNKNOWN` indefinitely (e.g. an endpoint whose polls keep failing) does not by itself pull the
|
|
66
|
+
* aggregate below `ONLINE` — a deliberate tradeoff to keep transient errors from flapping the
|
|
67
|
+
* indicator; a service that wants to be surfaced reports `FAILED` explicitly.
|
|
67
68
|
*/
|
|
68
69
|
export const aggregateConnectivity = (services) => {
|
|
69
70
|
const values = Object.values(services);
|
|
71
|
+
if (values.includes(ServiceConnection.FAILED))
|
|
72
|
+
return RobotConnectivity.DEGRADED;
|
|
70
73
|
const hasConnected = values.includes(ServiceConnection.CONNECTED);
|
|
71
74
|
const hasDisconnected = values.includes(ServiceConnection.DISCONNECTED);
|
|
72
75
|
if (hasConnected && hasDisconnected)
|
|
@@ -99,24 +102,32 @@ const connectivityEquals = (a, b) => {
|
|
|
99
102
|
};
|
|
100
103
|
/**
|
|
101
104
|
* Emits the robot's {@link RobotConnectivityStatus} — the per-service breakdown
|
|
102
|
-
* plus the aggregate verdict — for the configured `expectedServices
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
+
* plus the aggregate verdict — for the configured `expectedServices`, plus any
|
|
106
|
+
* caller-injected `extraServices` (folded in on equal footing). HTTP services
|
|
107
|
+
* are polled; `telemetry-liveness` and injected services use their supplied
|
|
108
|
+
* observable; any unrecognised service id resolves to `UNKNOWN`.
|
|
105
109
|
*/
|
|
106
110
|
export const getConnectivityChanges = (deps, projectId, callsign, expectedServices = DEFAULT_EXPECTED_SERVICES, pollMs = DEFAULT_CONNECTIVITY_POLL_MS) => {
|
|
111
|
+
const extraServices = deps.extraServices ?? {};
|
|
112
|
+
const serviceIds = [...expectedServices, ...Object.keys(extraServices).filter((id) => !expectedServices.includes(id))];
|
|
107
113
|
// combineLatest([]) completes without emitting; emit an explicit empty/offline status instead.
|
|
108
|
-
if (
|
|
114
|
+
if (serviceIds.length === 0) {
|
|
109
115
|
return of({ overall: RobotConnectivity.OFFLINE, services: {} });
|
|
110
116
|
}
|
|
111
|
-
const perService =
|
|
117
|
+
const perService = serviceIds.map((serviceId) => {
|
|
112
118
|
let source$;
|
|
113
119
|
if (serviceId === TELEMETRY_LIVENESS_SERVICE) {
|
|
114
120
|
source$ = deps.telemetryLiveness$;
|
|
115
121
|
}
|
|
116
|
-
else if (serviceId
|
|
122
|
+
else if (Object.prototype.hasOwnProperty.call(CONNECTION_SERVICE_PREFIXES, serviceId)) {
|
|
117
123
|
const url = buildConnectionsUrl(serviceId, deps.baseUrl, projectId, callsign, deps.insecure);
|
|
118
124
|
source$ = getServiceConnectionChanges(deps.httpGet, url, pollMs);
|
|
119
125
|
}
|
|
126
|
+
else if (Object.prototype.hasOwnProperty.call(extraServices, serviceId)) {
|
|
127
|
+
// Contain an injected observable's errors as UNKNOWN, mirroring the built-in sources, so a
|
|
128
|
+
// faulty caller signal can't error out combineLatest and tear down the whole aggregate.
|
|
129
|
+
source$ = extraServices[serviceId].pipe(catchError(() => of(ServiceConnection.UNKNOWN)));
|
|
130
|
+
}
|
|
120
131
|
else {
|
|
121
132
|
source$ = of(ServiceConnection.UNKNOWN);
|
|
122
133
|
}
|
|
@@ -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,9 +1,11 @@
|
|
|
1
1
|
import { catchError, combineLatest, distinctUntilChanged, filter, interval, map, merge, of, scan, startWith, } from 'rxjs';
|
|
2
|
-
import { RobotHealth, RobotReadiness, ServiceConnection } from '../../models';
|
|
2
|
+
import { RobotHealth, RobotReadiness, ServiceConnection, } from '../../models';
|
|
3
3
|
/** The new agent topic carrying health + readiness (replaces the heartbeat). */
|
|
4
4
|
export const ROBOT_STATUS_SOURCE = '/diagnostics/robot/status';
|
|
5
5
|
/** The legacy heartbeat topic — liveness only, no status payload. */
|
|
6
6
|
export const LEGACY_HEARTBEAT_SOURCE = '/rocos/agent/telemetry/heartbeat';
|
|
7
|
+
/** The per-contributor breakdown behind the reported status (health + readiness). */
|
|
8
|
+
export const ROBOT_STATUS_CONTRIBUTORS_SOURCE = `${ROBOT_STATUS_SOURCE}/contributors`;
|
|
7
9
|
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 5000;
|
|
8
10
|
const DEFAULT_INTERVAL_MS = 2000;
|
|
9
11
|
/**
|
|
@@ -78,6 +80,38 @@ export const getReportedStatusChanges = (telemetry, projectId, callsign) => {
|
|
|
78
80
|
// UNKNOWN/UNKNOWN (matching getTelemetryLiveness) so subscribers get an honest value.
|
|
79
81
|
catchError(() => of({ health: RobotHealth.UNKNOWN, readiness: RobotReadiness.UNKNOWN })));
|
|
80
82
|
};
|
|
83
|
+
const toContributor = (raw, parse) => ({
|
|
84
|
+
id: raw.id ?? '',
|
|
85
|
+
component: raw.component ?? '',
|
|
86
|
+
reason: raw.reason ?? '',
|
|
87
|
+
status: parse(raw.value),
|
|
88
|
+
});
|
|
89
|
+
const EMPTY_CONTRIBUTORS = { health: [], readiness: [] };
|
|
90
|
+
/**
|
|
91
|
+
* Emits the per-contributor breakdown behind the robot's reported health and
|
|
92
|
+
* readiness, from the agent's `robot/status/contributors` topic. Each
|
|
93
|
+
* contributor's integer wire value is mapped onto {@link RobotHealth} /
|
|
94
|
+
* {@link RobotReadiness}; a missing `health`/`readiness` array becomes `[]`.
|
|
95
|
+
*
|
|
96
|
+
* Starts with an empty breakdown so consumers combining this with other streams
|
|
97
|
+
* emit immediately, and — like {@link getReportedStatusChanges} — a telemetry
|
|
98
|
+
* error falls back to an empty breakdown rather than terminating the stream.
|
|
99
|
+
*/
|
|
100
|
+
export const getStatusContributorsChanges = (telemetry, projectId, callsign) => {
|
|
101
|
+
return telemetry
|
|
102
|
+
.subscribe({
|
|
103
|
+
projectId,
|
|
104
|
+
callsigns: [callsign],
|
|
105
|
+
sources: [ROBOT_STATUS_CONTRIBUTORS_SOURCE],
|
|
106
|
+
})
|
|
107
|
+
.pipe(map((message) => {
|
|
108
|
+
const payload = message.payload ?? {};
|
|
109
|
+
return {
|
|
110
|
+
health: (payload.health ?? []).map((c) => toContributor(c, parseRobotHealth)),
|
|
111
|
+
readiness: (payload.readiness ?? []).map((c) => toContributor(c, parseRobotReadiness)),
|
|
112
|
+
};
|
|
113
|
+
}), startWith(EMPTY_CONTRIBUTORS), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), catchError(() => of(EMPTY_CONTRIBUTORS)));
|
|
114
|
+
};
|
|
81
115
|
/**
|
|
82
116
|
* Emits the `telemetry-liveness` connectivity signal: whether a heartbeat (from
|
|
83
117
|
* either the new `robot/status` topic or the legacy heartbeat) has been received
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dronedeploy/rocos-js-sdk",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.6",
|
|
4
4
|
"description": "Javascript SDK for rocos",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "esm/index.js",
|
|
@@ -42,5 +42,10 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"repository": {},
|
|
45
|
-
"private": false
|
|
45
|
+
"private": false,
|
|
46
|
+
"overrides": {
|
|
47
|
+
"openapi-typescript": {
|
|
48
|
+
"typescript": "$typescript"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
46
51
|
}
|